Using infer
infer reaches inside another type and binds a name to a piece of it. Position decides what gets caught.
You have a function and you want its return type — without writing it twice. The lib gives you ReturnType<F>, and once you've read its definition, you'll never look at the type system the same way. The trick is a keyword called infer, which only works inside conditional types and lets you pluck a piece out of a larger shape.
What infer does
infer X introduces a fresh type variable. You can only use it on the right side of an extends clause, inside a conditional type. TypeScript matches the conditional like a pattern, and wherever it sees infer X, it binds the matched piece to that name. You then refer to X in the true branch.
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never; type A = ReturnType<() => number>; // number type B = ReturnType<(x: string) => string[]>; // string[] type C = ReturnType<"not a function">; // never
Read the conditional like a pattern: does F look like a function with any arguments returning something? If yes, capture that something as R and hand it back. If no, give never. The (...args: any[]) part says I don't care about the parameter list right now, just match it. The infer R slot is the only piece we want to pull out.
This is structural pattern matching. The right side of extends is a shape with a hole in it, and infer R is the hole. TypeScript only resolves the conditional if the shape on the left fits the shape on the right; when it does, R becomes whatever filled the hole.
Building Awaited
Promises chain. Promise<Promise<string>> is something you genuinely run into when working with async code. The Awaited<T> utility unwraps however many promise layers there are. A first version handles a single layer.
type Awaited<T> = T extends Promise<infer U> ? U : T; type A = Awaited<Promise<number>>; // number type B = Awaited<string>; // string (passes through) type C = Awaited<Promise<Promise<string>>>; // Promise<string> (only one layer!)
The pattern matches Promise<infer U> and pulls out U. So Promise<number> matches with U = number. A non-promise like string doesn't match, so the false branch hands the type back unchanged.
The single-layer version misses nested promises. To handle them, the type calls itself recursively — recursion at the type level is a real thing, covered properly in lesson 5. For now just notice the pattern.
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T; type R = Awaited<Promise<Promise<Promise<string>>>>; // string
type A<T> = T extends (a: infer P) => any ? P : never;, what is A<(name: string) => void>?Pulling element types
Arrays are the other place infer shines. You can ask: given an array type, what kind of thing is in it?
type ElementType<T> = T extends (infer E)[] ? E : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<Array<{ id: number }>>; // { id: number }The pattern is (infer E)[] — some element type, then square brackets. When T is string[], the slot fills with string, so E = string. The parentheses around infer E are needed because [] would otherwise read as part of the type.
You can pull from tuple positions too. The first element, the rest of the elements, or the last — all are real patterns that show up in library type definitions.
type Head<T> = T extends [infer H, ...any[]] ? H : never; type Tail<T> = T extends [any, ...infer R] ? R : never; type A = Head<[1, 2, 3]>; // 1 type B = Tail<[1, 2, 3]>; // [2, 3]
Head says match something that starts with one element followed by any number of others. Tail says match anything followed by some rest, capturing only the rest. The ... rest syntax in tuple types lets you say any number of elements here and bind that whole stretch with infer.
Position is the whole rule
Where you put infer decides what you capture. Two conditional types can look almost identical and produce wildly different things.
type FirstParam<F> = F extends (a: infer A, ...rest: any[]) => any ? A : never; type Returned<F> = F extends (...args: any[]) => infer R ? R : never; type Fn = (id: number, name: string) => boolean; type X = FirstParam<Fn>; // number type Y = Returned<Fn>; // boolean
Both check the same kind of shape — a function. The only difference is which slot gets the infer. The first puts it in the parameter position, so it captures number. The second puts it in the return position, so it captures boolean.
When a library type confuses you, find the infer and ask what is this slot in the pattern? Whatever fills that slot is what the type produces.
You can use multiple infer slots in the same conditional. Each one binds its own variable, and you can use all of them in the true branch.
type FirstAndSecond<T> = T extends [infer A, infer B, ...any[]] ? [A, B] : never; type R = FirstAndSecond<[string, number, boolean]>; // [string, number]
Two slots, two captures, one combined result. The tuple [A, B] in the true branch is built from the two pieces the pattern pulled out.
infer is only legal inside a conditional type. Writing type Bad<T> = (a: infer A) => A at the top level is a syntax error.
Try it yourself
type R = ElementType<string>; produce, where ElementType<T> = T extends (infer E)[] ? E : never?type Last<T> = T extends [...any[], infer L] ? L : never;. What is Last<[1, 2, 3]>?type Bad<F> = (a: infer A) => A; compile?