infer in TypeScript: Capturing Types Inside Conditional Types
We use infer to capture pieces of a matching type, write ReturnType, Parameters and Awaited ourselves, match tuple and string patterns, constrain infer and avoid the overload pitfall.
Version note: The code in this post targets TypeScript 7.0 and Node 26.
Predict First
We wrote a small type that extracts the element type of an array type:
type ElementOf<T> = T extends (infer Item)[] ? Item : never;
type A = ElementOf<string[]>;
type B = ElementOf<[number, boolean]>;
type C = ElementOf<readonly string[]>;
Which types are A, B and C? You probably already have an answer for A. B is a tuple; does it match the pattern, and if so, what gets captured? C is the readonly version of A; do you think it gives the same result? The answers come after we see how infer works.
Pattern Matching: What Does infer Do?
In the previous post we used conditional types to ask whether a type matches a pattern. infer arrived together with conditional types in TypeScript 2.8, and it adds something to that question: "If it matches, capture the type that lands in this slot and give it to me under a name."
Remember the destructuring we saw in the JavaScript series. When you write const [first] = items;, you look at the shape of the value on the right and bind a piece of it to a variable. infer does the same for types. T extends (infer Item)[] reads as: "if T is an array, call the type of its elements Item." The captured Item can only be used in the true branch of the condition; if the pattern doesn't match, the false branch runs and there is no Item there.
The first two questions from the prediction are solved right here. For A, string[] matches the pattern and Item becomes string. In B, a tuple is also an array; [number, boolean] is assignable to (number | boolean)[], so it matches and the result is number | boolean. The answer to C is in the pitfalls section.
infer can only be written in the extends clause of a conditional type. Use it anywhere else and the compiler tells you plainly:
// Error: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
type Broken<T> = infer U;
The code for this error is TS1338. So infer does nothing on its own; it always marks a slot inside a pattern.
Writing Your Own ReturnType
In the Intermediate series we used ReturnType and Parameters off the shelf. Now let's write both ourselves:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
function createUser(name: string, age: number) {
return { name, age, active: true };
}
type NewUser = MyReturnType<typeof createUser>; // { name: string; age: number; active: boolean; }
type CreateUserArgs = MyParameters<typeof createUser>; // [name: string, age: number]
The pattern is "a function that takes any parameter list and returns something". In MyReturnType we put the slot on the return type, in MyParameters on the parameter list. Parameters are captured as a tuple, and the parameter names are even kept as named tuple labels. typeof createUser is the type-level typeof we saw in the Intermediate series: the way to get from a value to a type.
Using any[] in the pattern is deliberate. Had we written unknown[], functions with specific parameter types would not match because of contravariance (we'll discuss it in detail later in the series). The real definitions in lib are very close to ours; the only difference is that they put a constraint on T:
// From lib.es5.d.ts; they are already global, so don't copy them into your own file
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Thanks to that constraint, writing ReturnType<string> makes the compiler report the error on the spot instead of returning never: "Type 'string' does not satisfy the constraint '(...args: any) => any'." In your own types, too, blocking misuse with a constraint is usually better than silently turning it into never.
Writing Your Own Awaited
The return type of an async function is a Promise. Most of the time, though, what we need is the type of the value inside the promise:
type MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T;
type N = MyAwaited<Promise<Promise<number>>>; // number
type S = MyAwaited<string>; // string
async function loadUser() {
return { id: 1, name: "Ada" };
}
type User = MyAwaited<ReturnType<typeof loadUser>>; // { id: number; name: string; }
function greet(user: User) {
console.log(`Hello, ${user.name}`);
}
loadUser().then(greet); // Hello, Ada
There's something new here: MyAwaited calls itself in its true branch. We peel nested promises layer by layer, the way await does. If it isn't a promise, we return the type itself, because await 5 also gives 5 in JavaScript. We'll devote a separate post to self-referencing types later in the series.
The real Awaited, introduced in TypeScript 4.5, is a bit more thorough: instead of Promise it unwraps any object with a then method (a thenable), because that's how await works in JavaScript too. The idea is the same: capture the matching layer with infer, and repeat if needed.
infer in Tuples
In tuple patterns you can place infer at any position, together with rest elements:
type First<T> = T extends [infer Head, ...unknown[]] ? Head : never;
type Last<T> = T extends [...unknown[], infer Tail] ? Tail : never;
type Rest<T> = T extends [unknown, ...infer R] ? R : never;
type F = First<[string, number, boolean]>; // string
type L = Last<[string, number, boolean]>; // boolean
type R = Rest<[string, number, boolean]>; // [number, boolean]
type E1 = First<[]>; // never
type E2 = First<string[]>; // never
First<[]> makes sense: an empty tuple has no first element. First<string[]> is more interesting. A string[] may have zero elements; since it's not guaranteed to match the "has at least one element" pattern, the false branch is chosen. Tuple patterns work best with types whose length is known.
infer in Strings
infer also works inside template literal types. We'll look at template literal types in detail two posts from now; for the moment it's enough to know they are the type-level counterpart of JavaScript's template literals:
type Method<T> = T extends `${infer M} ${string}` ? M : never;
type M1 = Method<"GET /users">; // "GET"
type SplitDot<T> = T extends `${infer Head}.${infer Tail}` ? [Head, Tail] : [T];
type P1 = SplitDot<"user.address.city">; // ["user", "address.city"]
Method captures the part before the space. Pay attention to the result of SplitDot: Head takes the shortest piece up to the first dot, and everything else goes to Tail. It starts from the left and stops at the first match. To split all the pieces, you'd need to call the same type again on Tail, which again leads to the topic of self-referencing types.
Adding a Constraint to infer
Since TypeScript 4.7 we can write a constraint directly on infer. If the captured type doesn't satisfy the constraint, the pattern counts as not matching:
type FirstString<T> = T extends [infer F extends string, ...unknown[]] ? F : never;
type FS1 = FirstString<["Ada", 1]>; // "Ada"
type FS2 = FirstString<[1, "Ada"]>; // never
type ToNumber<T> = T extends `${infer N extends number}` ? N : never;
type T1 = ToNumber<"42">; // 42
type T2 = ToNumber<"1.0">; // number
type T3 = ToNumber<"abc">; // never
ToNumber uses a subtlety introduced in TypeScript 4.8: an infer constrained to number inside a template literal tries to turn the string into a literal number type. For "42" the result is 42. For "1.0" it's just number, because the compiler only produces a literal for values that stay the same after converting to a number and back to a string, and String(Number("1.0")) gives "1". "abc" isn't a number at all, so the pattern doesn't match.
One Name in Two Places: Union or Intersection?
You can use the same infer variable in more than one place in a pattern. The compiler then finds several candidates and has to combine them. How it combines them depends on the position the variable is in:
type PropUnion<T> = T extends { a: infer U; b: infer U } ? U : never;
type U1 = PropUnion<{ a: string; b: number }>; // string | number
type ParamBoth<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type U2 = ParamBoth<{ a: (x: { id: number }) => void; b: (x: { name: string }) => void }>; // { id: number; } & { name: string; }
In a property position, the two candidates combine into a union: the value can be either a string or a number. In a function parameter position they combine into an intersection: the only value you can pass to both functions is one that satisfies both expectations at once. Run the same experiment with string and number parameters and you get string & number, which is never. This difference between positions is called variance; we'll cover it in a separate post later in the series.
Common Pitfalls
Let's start with C from the prediction: ElementOf<readonly string[]> is never. A readonly array is not assignable to a regular array (it has no methods like push), so it doesn't match the (infer Item)[] pattern. Write the pattern as readonly (infer Item)[] and you catch both kinds, because a regular array is assignable to a readonly one.
The second pitfall is overloads. When you apply infer to an overloaded function, the compiler only looks at the last signature:
function parse(input: string): number;
function parse(input: number): string;
function parse(input: string | number) {
return typeof input === "string" ? Number(input) : String(input);
}
type Parsed = ReturnType<typeof parse>; // string
console.log(parse("42") + 1); // 43
parse("42") actually matches the signature that returns number, but ReturnType sees the string from the last signature. Don't rely on ReturnType and Parameters with overloaded functions.
The third is what you return in the false branch. Returning never says "something that doesn't match this pattern shouldn't exist", and it can silently hide a mistake. If inputs that don't match really make no sense, it's better to do what lib does: put a constraint on the generic parameter and show the error where it's used.
A Small Experiment
★☆☆ Predict, then verify: with type Unwrap<T> = T extends Set<infer V> ? V : T;, which types are Unwrap<Set<"a" | "b">> and Unwrap<string>?
★★☆ Write FirstArg<T>, which gives only the type of a function's first parameter. FirstArg<(id: number, name: string) => void> should be number.
★★★ Write a TrimLeft<S extends string> that removes leading spaces: the pattern is ` ${infer Rest}`, and in the true branch it calls itself with Rest. TrimLeft<" hello"> should be "hello".
In this post we took a type and pulled a single piece out of it. In the next post we'll walk over every property of an object type one by one and produce a brand-new object type: mapped types, the readonly and ? modifiers, and renaming keys with as.