Command Palette

Search for a command to run...

Hectal
PHASE 9Advanced ~27 min· topic 3 of 5

Topic 9.3

The infer Keyword

In one line

Inside the extends part of a conditional type, infer X says "whatever type sits here, call it X", so you can pull out element types, return types and promise values.

Think of it like this

A claim ticket at a coat check. You hand over a coat and get a ticket with a number; later the ticket lets you get back exactly what was inside. **infer is a claim ticket for part of a type.** You describe the shape ("an array of something"), put a ticket on the "something" (infer E), and if the shape matches, the ticket gives you that part back to use in the answer.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

infer
A keyword that names a piece of a type while the compiler matches a pattern, so you can use that piece in the answer.
Pattern
The shape written after extends, like Promise<infer V> or (infer E)[].
Fallback
The answer in the "no" branch, used when the shape does not match.
Return type
The type of the value a function gives back.
Tuple
A fixed-length array type where each position has its own type, like [string, number].
Covariant position
A spot where a more specific type is allowed, like a return type. Several candidates make a union.
Contravariant position
A spot like a function parameter, where the direction flips. Several candidates make an intersection.
Constraint (on infer)
infer X extends T: the inferred piece must also fit T, otherwise the pattern does not match.

Step by step

01Ticket an array's items

Match the shape "array of something" and name the something E. The answer is E.

For string[], E is string. For a type that isn't an array, the pattern fails and the fallback never is used.

element-of.tswhole filets
type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;             // string
type B = ElementOf<(number | boolean)[]>; // number | boolean
type C = ElementOf<string>;               // never

02Unwrap a promise

Promise<infer V> pulls the value out of a promise type. Here the fallback is T itself, so a plain boolean passes through.

Notice Unwrap<Promise<Promise<number>>> gives Promise<number>: it peels one layer. Peeling all layers needs recursion (Topic 9.5), and it's what the built-in Awaited does.

unwrap.tswhole filets
type Unwrap<T> = T extends Promise<infer V> ? V : T;

type P1 = Unwrap<Promise<string>>; // string
type P3 = Unwrap<boolean>;         // boolean

03Rebuild ReturnType and Parameters

For a function, ticket the return type with infer R and the whole parameter list with infer P. Recall from Topic 8.2: typeof makeUser is the type of the function value. P is a tuple (Topic 5.2: a fixed-length array type), and its items keep their names: [name: string, age: number].

never[] and unknown in the pattern make the matching as wide as possible, so any function fits. Why never[] and not unknown[]? A function parameter is a contravariant spot (Topic 7.7): a function that needs a string is only safe where callers will pass strings. With unknown[] callers might pass anything, so makeUser would be rejected and the answer would be never. With never[] no caller can pass any value at all, so every function qualifies.

return-and-params.tswhole filets
type MyReturn<F> = F extends (...args: never[]) => infer R ? R : never;
type MyParams<F> = F extends (...args: infer P) => unknown ? P : never;

function makeUser(name: string, age: number) {
  return { name, age, active: true };
}
type User = MyReturn<typeof makeUser>; // { name: string; age: number; active: boolean }
type Args = MyParams<typeof makeUser>; // [name: string, age: number]
Rebuild ReturnType and Parametersdiagram
Rendering diagram…

04Pick tuple positions

[infer H, ...unknown[]] means "a first item, then anything". [...unknown[], infer L] means "anything, then a last item".

Add extends number to the infer and the match works only when that item fits: FirstNumber<["x", 42]> takes the fallback.

05Two tickets with the same name

In return positions, two candidates make a union. In parameter positions, they make an intersection, and string & number collapses to never. This is the root of a famous trick that turns a union into an intersection.

Try it yourself

  1. 1

    Get the second parameter

    Write SecondParam<F> that gives the type of a function's second parameter. Try it on makeUser. Expect number.

    Hint: infer the parameter list first, then match [unknown, infer S, ...unknown[]].

  2. 2

    Peel promises

    Make Unwrap recursive: in the yes branch, return Unwrap<V>. Check Unwrap<Promise<Promise<number>>> now gives number.

  3. 3

    Change the fallback

    In ElementOf, change never to T. Predict ElementOf<string>, then check. This is the difference between "fail loudly" and "pass through".

Code & diagrams

Pulling out element types, promise values, return types and parameters

Each infer names one piece. Check that the fallback appears when the shape does not match (C and NotFn are never).

Console

(nothing was printed)
Tuple positions and infer ... extends TypeScript 4.7+

infer H extends number keeps the exact literal 42. The text "5" becomes the number type 5, and "five" does not parse as a number, so it takes the fallback.

Console

(nothing was printed)
The same name twice: union or intersection

Return positions combine into a union (string | number). Parameter positions combine into an intersection, and string & number is never. LastArg first tickets the whole parameter list, then its last item.

Console

(nothing was printed)

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A pattern that is too narrow

In the first example, change the pattern of MyReturn from (...args: never[]) => infer R to (...args: string[]) => infer R. The call MyReturn<typeof makeUser> passes a function that takes a string and a number.

terminal
$ Types panel
── what you'll see ──
index.ts(24,6): type User = never

Myth vs fact

Myth

infer declares a new type variable anywhere in a type.

Fact

Only inside the extends clause of a conditional type, and the name is only visible in the true branch.

Myth

infer always gives back the exact type.

Fact

It gives the best match for the pattern. With several candidates it makes a union (covariant) or an intersection (contravariant), which may not be what you expected.

Myth

If infer fails I get an error.

Fact

No. A failed match silently takes the false branch, so check the fallback type in the Types panel.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    infer X extends T (4.7) removed the nested conditional type that was needed to constrain an inferred piece. It also affects what is inferred: infer N extends number in a template literal makes the compiler try to parse the text as a number, and a text that does not survive the round trip, like "05", gives the plain type number instead of a literal.

  • ▸

    The union-to-intersection trick used in library code relies on the contravariant rule: put each member in a function parameter, then infer the parameter once. The members are intersected. It works, but it is the kind of cleverness Topic 10.6 warns about.

  • ▸

    Overloaded functions: infer on a function with several overloads sees only the last overload. ReturnType of an overloaded function therefore gives the return type of the final signature.

Remember this

  1. 1

    The problem this solves: a conditional type (Topic 9.1) can ask "is this an array?", but not "an array of what?". Yet you often need that inner piece: the item type of an array, the value inside a Promise, the type a function returns. infer lets you name that piece so the answer can use it. Without it, you would have to write a separate type for every case by hand.

  2. 2

    infer can only be written in the extends part of a conditional type. T extends (infer E)[] ? E : never reads: "if T is an array of something, call that something E and answer E; otherwise never". The name E exists only in the "yes" branch. Writing infer anywhere else is an error (TS1338).

  3. 3

    The pattern matching works by shape. ElementOf<string[]> matches (infer E)[] with E = string. ElementOf<string> does not match (a string is not an array), so you get the fallback, never. Always decide the fallback: never for "this should not happen", or T itself when you want to pass other types through unchanged, like Unwrap<boolean> below.

  4. 4

    This is exactly how ReturnType<F> and Parameters<F> are written. F extends (...args: never[]) => infer R ? R : never asks: "is F a function? If so, call its return type R." You can put infer in the parameters ((...args: infer P)), in a promise (Promise<infer V>), in tuple positions ([infer H, ...unknown[]], [...unknown[], infer L]) and in template strings (Topic 9.4).

  5. 5

    If the same name is inferred in two places, TypeScript combines the candidates. In a covariant spot (a return type, a property) the result is a union: string | number. In a contravariant spot (a function parameter) the result is an intersection: string & number, which is never (Topic 7.7 explained the two directions). It's a small surprise worth knowing before you see it in a library.

  6. 6

    **infer X extends T** (TypeScript 4.7) adds a constraint: infer a piece only if it also fits T. [infer H extends number, ...unknown[]] matches only tuples whose first item is a number, and H keeps the exact literal (42). With template strings, ` ${infer N extends number} turns the text "5" into the number type 5`. Before 4.7 you needed a second nested conditional type.

  7. 7

    Matching is strict about what is expected. If you ask for a function that takes strings ((...args: string[]) => infer R) and give it one taking a string and a number, the shapes don't match and you get the fallback. Check the Types panel when an infer returns never unexpectedly: the pattern is usually a little too narrow.

Explain it without notes

01

What does infer do, and where may it be written?

02

How is ReturnType<F> written, step by step?

03

What happens when the same name is inferred in two positions?

04

What does infer X extends T add, and since which version?

Practice

01

Write Last<T> giving the last item of a tuple type and test it on [1, 2, 3].

02

Write PromiseValue<F> for an async function type: the value its promise resolves to. Test it on async () => 42.

03

Write Tail<T extends unknown[]> that returns the tuple without its first item.

Trade-offs

  • ↔

    infer makes small, reusable extractors, but each one is a puzzle for a reader. Prefer the built-ins (ReturnType, Parameters, Awaited) when they exist.

Done when you can

  • I can say where infer may be used and where its name is visible.

  • I can write extractors for array items, promise values and return types.

  • I can pick tuple positions with infer.

  • I can explain union vs intersection for repeated infer names.

  • I can use infer X extends T and name the version it arrived in.