TypeScript Guide EP13: The Infer Keyword
Video
Watch the full video on YouTube:
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:
T extends (...args: unknown[]) => unknown- the genericTmust have a function signature (with or without params, returning anything)- We evaluate
Tagain, but this time for inference, not validation (...args: unknown[]) => infer R- we inferR, the last element of the signature: the return value- If
Rexists, we return it; otherwise we returnunknown
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:
T extends Promise<infer U>- we ask ifTextends a promiseinfer U- we infer the type living inside the promise- If
Ucan be inferred, we returnU; otherwise we returnnever
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:
T extends string- this type only operates on strings (key names)`${infer Head}_${infer Tail}`- we infer two segments separated by an underscore: the head (Head) and the tail (Tail)- If it matches, we concatenate
Headwith the capitalizedTail - We apply recursion:
SnakeToCamelCase<Tail>- we don’t know how many underscores there are, so we repeat the question for each one - If it doesn’t match (the
portcase in the practical example, no underscores), we returnTas-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:
[P in keyof T]- we iterate every property of objectT- We operate on
P(the key), validating thatPextendsstring - If it’s a string, we transform it with
SnakeToCamelCase<P>; otherwise we useneverto discard it - 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
- Use
inferinside conditional types: always in theT extends Pattern ? ... : ...shape - Name inferred variables with semantic keys (
Rfor return,U/Tfor generics) so the definition stays readable - Prefer
unknownoveranyin signatures you write yourself; modern TypeScript’s realReturnTypeusesunknown - Combine recursion with
inferwhen you don’t know the pattern depth (multiple underscores, nested promises) - Return
neverwhen the pattern can’t be inferred and the case is invalid; useTitself as the fallback when the case is valid but not a special pattern - No generics, no infer:
inferinspects 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
