Video

Watch the full video on YouTube:

Infer Keyword in TypeScript

Introduction

In this thirteenth chapter of our TypeScript series, we introduce the infer keyword, one of the most powerful tools in the language for extracting segments from compound types.

With infer we can evaluate a type expression, capture a concrete fragment (for example, a function’s return type or the content of a promise), and return it. We’ve already used it without knowing: the ReturnType utility type uses it internally. In this episode we’ll read its definition and build our own types with infer: a homemade Awaited and a snake_case to camelCase key transformer.

What is the Infer Keyword

infer lets us extract a particular segment from a compound type, evaluate it, and return it. For example: get the type at the third nesting level of an object, or the type a promise resolves to.

It works inside conditional types, and we always declare a new type variable at inference time:

// Question: does T extend a function signature?
// If so, infer the return type into R and return it
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : unknown

The mental model is: we evaluate an expression and infer an element. If the answer is yes, we return it. If it’s no, we return something pertinent (or never).

Return Type Uses Infer Under the Hood

If we inspect the real definition of ReturnType, we find infer near the end:

type ReturnType<T extends (...args: unknown[]) => unknown> =
  T extends (...args: unknown[]) => infer R ? R : unknown

Step-by-step explanation:

  1. T extends (...args: unknown[]) => unknown - the generic T must have a function signature (with or without params, returning anything)
  2. We evaluate T again, but this time for inference, not validation
  3. (...args: unknown[]) => infer R - we infer R, the last element of the signature: the return value
  4. If R exists, we return it; otherwise we return unknown

We know R is the return value because it’s the last expression of the signature: parentheses (args), arrow, return.

Implementing Awaited with Infer

The Awaited utility type returns the value a promise resolves to. We’ll create our own version.

Example function

async function resolve() {
  return { id: 1, name: "angel" }
}

Adding async in front of the definition turns the function into a promise.

Using the built-in Awaited

type ResolvedAwaited = Awaited<ReturnType<typeof resolve>>
// { id: number, name: string }

We pass the promise definition (via ReturnType) to Awaited, which resolves it.

Our own implementation

type Awaiting<T> = T extends Promise<infer U> ? U : never

Explanation:

  1. T extends Promise<infer U> - we ask if T extends a promise
  2. infer U - we infer the type living inside the promise
  3. If U can be inferred, we return U; otherwise we return never

never is something that shouldn’t happen, something that can’t be typed or evaluated. If we don’t pass a promise, we get never because it can’t resolve the inner value.

Improving the fallback

If we want it to return the type as-is when it’s not a promise:

type Awaiting<T> = T extends Promise<infer U> ? U : T
type FromPromise = Awaiting<ReturnType<typeof resolve>>
// { id: number, name: string }

type NotAPromise = Awaiting<string>
// string

If it’s a promise, it resolves the inside. If it’s not a promise, it returns what came in.

Transforming snake_case to camelCase

A more advanced use of infer: transforming attribute formats. We’ll create a type that converts snake_case keys to camelCase.

SnakeToCamelCase

type SnakeToCamelCase<T extends string> =
  T extends `${infer Head}_${infer Tail}`
    ? `${Head}${Capitalize<SnakeToCamelCase<Tail>>}`
    : T

Step-by-step explanation:

  1. T extends string - this type only operates on strings (key names)
  2. `${infer Head}_${infer Tail}` - we infer two segments separated by an underscore: the head (Head) and the tail (Tail)
  3. If it matches, we concatenate Head with the capitalized Tail
  4. We apply recursion: SnakeToCamelCase<Tail> - we don’t know how many underscores there are, so we repeat the question for each one
  5. If it doesn’t match (the port case in the practical example, no underscores), we return T as-is

ObjectToCamelCase

Now we read a full object and transform each of its keys:

type ObjectToCamelCase<T> = {
  [P in keyof T as P extends string ? SnakeToCamelCase<P> : never]: T[P]
}

Explanation:

  1. [P in keyof T] - we iterate every property of object T
  2. We operate on P (the key), validating that P extends string
  3. If it’s a string, we transform it with SnakeToCamelCase<P>; otherwise we use never to discard it
  4. On the right side we keep the original value T[P]

Practical example

type AllConfig = {
  user_id: number
  database_connection_string: string
  port: number
}

const config: ObjectToCamelCase<AllConfig> = {
  userId: 1,
  databaseConnectionString: "postgres://localhost/db",
  port: 3000
}

We get exactly userId and databaseConnectionString, regardless of how many underscores there are, and port passes unchanged because it has no underscore. Every attribute stays strictly typed.

Best Practices

  1. Use infer inside conditional types: always in the T extends Pattern ? ... : ... shape
  2. Name inferred variables with semantic keys (R for return, U/T for generics) so the definition stays readable
  3. Prefer unknown over any in signatures you write yourself; modern TypeScript’s real ReturnType uses unknown
  4. Combine recursion with infer when you don’t know the pattern depth (multiple underscores, nested promises)
  5. Return never when the pattern can’t be inferred and the case is invalid; use T itself as the fallback when the case is valid but not a special pattern
  6. No generics, no infer: infer inspects generic data, so you’ll almost always be working with generic types

Conclusion

In this episode we learned that infer evaluates a type expression and extracts a concrete element. We saw that ReturnType already uses it under the hood, we implemented a homemade Awaited to resolve promises, and we built a recursive snake_case to camelCase transformer that operates on both strings and object keys without losing type inference.

infer is very powerful, but it’s designed to work in a very specific way: inspecting deep generic data and creating new transformed data without losing type inference.

If something wasn’t clear or you want more detail on these implementations, leave it in the comments section.

Repository

💻 Repository: https://github.com/radagva/ts-example

NestJS Guide EP10: Dependency Injection and ProvidersTypeScript Guide EP14: The Satisfies Operator