Lesson 43·M5 Type-Level Programming
infer
OpenLearning Objectives
By the end of this lesson you should be able to:
- Read a conditional type out loud, including the
inferclause - Reimplement
ReturnType<T>from scratch and explain each token - Reimplement
Awaited<T>,ElementType<T>andFlatten<T> - Explain where
infercan appear (and where it can't) - Recognise when an interview answer needs
infer— and when it doesn't
What Is infer?
infer declares a type variable inside a conditional type's extends clause and lets
TypeScript figure out what it should be from the type being matched.
Every conditional type is a pattern match:
T extends Pattern ? Match : No-match
└─────┬────┘
`infer X` names a hole in the pattern
TypeScript solves for X from T
If the pattern fits, infer X is bound to whatever part of T lined up with it and is
usable in the true branch. If the pattern doesn't fit, the whole conditional collapses to
the false branch and X never exists there.
Mental Model
Think of infer as destructuring for types.
When you write const [first, ...rest] = arr, JavaScript splits an array and binds names to
the pieces. T extends [infer Head, ...infer Tail] ? … does the same thing to a tuple type —
bind Head to the first element, bind Tail to the rest. A runtime pattern-match with
variables; the variables are just declared with infer instead of const.
Visual Flow
type ReturnType<T> =
T extends (...args: any[]) => infer R
│ │
│ solve for R │
▼ ▼
┌────────────────┐ ┌────────────────┐
│ T is a fn │ │ T is NOT a │
│ signature │ │ fn signature │
└───────┬────────┘ └───────┬────────┘
│ │
▼ ▼
R (the return type) never (no match)How It Works
ReturnType is defined in the standard library, but here is the whole thing, written out:
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type A = MyReturnType<() => string>; // string
type B = MyReturnType<(x: number) => number>; // number
type C = MyReturnType<number>; // never
Output:
A = string
B = number
C = never
Line by line:
type MyReturnType<T> =
// 1. T must match a function signature: (...args: any[]) => …
T extends (...args: any[]) => infer R
// 2. …where `infer R` is a hole where the return type sits.
// If the signature fits, TypeScript binds R to the real return type.
? R // 3. Match → hand back the solved R
: never; // 4. No match → never (there was no return type to find)
Deep dive
any[] in the parameter slot is deliberate. We want to match any function, regardless
of its parameters, so we accept them loosely with any[]. That widens the parameters to a
rest of any — it costs you nothing here, because we only extract the return side. The
false branch is never (Lesson 45) rather than a fallback type, because "not a function"
has no return type at all.
Also note: infer R is declared in the condition but used in the true branch. That is
the whole trick — the scope of R is exactly the conditional's ? branch.
More than one infer in a pattern
A pattern can have several holes; TypeScript solves for all of them at once:
type Swap<Pair extends readonly [any, any]> =
Pair extends readonly [infer A, infer B] ? [B, A] : never;
type Swapped = Swap<['left', 'right']>; // ['right', 'left']
Output:
Swapped = ['right', 'left']
A and B are bound by position — the first slot, then the second — and reused in the true
branch in whatever order you like.
Reimplementing the Useful Ones
The flagship material. These four are the infer patterns interviewers actually reach for.
ReturnType<T> — the classic
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
declare function fetchUser(id: string): Promise<{ id: string; name: string }>;
type User = MyReturnType<typeof fetchUser>;
Output:
User = Promise<{ id: string; name: string }>
Awaited<T> — unwrap promises, recursively
type MyAwaited<T> =
T extends Promise<infer U> ? MyAwaited<U> : T;
type A = MyAwaited<Promise<string>>; // string
type B = MyAwaited<Promise<Promise<number>>>; // number
type C = MyAwaited<Promise<Promise<Promise<boolean>>>>; // boolean
Output:
A = string
B = number
C = boolean
The ? MyAwaited<U> branch is a recursive call: keep unwrapping until the outer Promise
is gone, then fall through to the plain T. This is the exact pattern the standard library's
Awaited<T> (the type await uses, and the one behind Promise.all inference) is built on.
ElementType<T> — the element of any collection
type ElementType<T> =
T extends (infer E)[] ? E
: T extends ReadonlyArray<infer E> ? E
: T extends Promise<infer E> ? E
: never;
type A = ElementType<string[]>; // string
type B = ElementType<ReadonlyArray<number>>; // number
type C = ElementType<Promise<boolean>>; // boolean
Output:
A = string
B = number
C = boolean
One infer variable can be reused across branches of the same conditional — but each branch
redeclares it with its own infer E. Note the two array branches: the mutable one first,
then ReadonlyArray. Order matters, because a readonly array also matches the first
pattern with any elements.
Flatten<T> — the interview favourite
type Flatten<T> =
T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[]>; // number
type C = Flatten<'flat'>; // 'flat' — no match, handed back as-is
type D = Flatten<string[][]>; // string[] — flattens one level only
Output:
A = string
B = number
C = 'flat'
D = string[]
That is the whole of Flatten. It's the go-to starter exercise for infer because the
shape is tiny but every moving part is visible.
Real Project Usage
| Where | The infer at work |
|---|---|
ReturnType<typeof fn> in tests/helpers | Extract what a function returns without annotating it twice |
Awaited<T> on API response types | Turn Promise<Response> into the response type itself |
Unwrapping React.ComponentProps<typeof X> | Pulls the prop type out of a component |
| Event map patterns (Lesson 42) | T extends { [K]: infer E } binds the payload of each event |
| Form/route tuples | Tuple extends [infer First, ...infer Rest] splits a fixed shape |
The everyday superpower: you never write the same type twice. A helper that takes the typeof of a function, an element, or a promise and returns its inside type is small, reusable, and reads better than a hand-written duplicate annotation.
Interview Explanation
Conditional types run a pattern match:
T extends Pattern ? A : B.infer Xdeclares a variable inside the pattern. TypeScript tries to fitTinto that pattern; where it lines up withX,Xis bound to that piece and is available in theAbranch. If the pattern doesn't fit, we takeBandXdoesn't exist there.The canonical example is
ReturnType<T>:T extends (...args: any[]) => infer R ? R : never. For a function type,Rbecomes the return type. For anything else, the pattern fails and we getnever.
That, plus a live reimplementation, is a senior-grade answer in under thirty seconds.
Senior-Level Insights
inferis local to one conditional. Eachinferdeclaration's scope is exactly the?branch of the conditional that contains it. You can't name it outside, and you can't reuse it in a sibling conditional.- Recursion is how you flatten.
Awaited<T>recurses throughPromiselayers;Flatten's non-recursive form only strips one level. Interviewers escalate "reimplementReturnType" into "make it recursive" on purpose. - Solve for the holes, not the whole. A conditional matches structurally — tuple
position, array element, promise payload — and
infercaptures exactly the hole you named. Nothing else inTis bound. anymatches anything,nevermatches nothing. If you want "any function", match(...args: any[]) => …. If nothing should match, the false branch isnever— and it can't match because nothing lines up withinfer Xwhen the pattern already failed.- Know when NOT to use it. If you just need "the return type of this one function", a
plain
ReturnTypeis better than a bespoke type.inferearns its place in reusable utilities, not one-off annotations.
Common Mistakes
❌ Using infer outside an extends clause — a syntax error.
type Bad<T> = infer X; // 💥 'infer' declarations are only permitted in the 'extends' clause of a conditional type
❌ Declaring two infers with the same name in one pattern — duplicate bindings collide.
type Bad<T> = T extends [infer X, infer X] ? X : never; // 💥 duplicate identifier
❌ Forgetting that a failed match produces the false branch, so infer X is unreachable
there — referencing it in the false branch is an error, and it is not unknown or any.
❌ Reaching for any instead of infer when you need the inside of a type.
type Bad<T> = T extends Array<any> ? any : T; // loses the element type
type Good<T> = T extends Array<infer E> ? E : T; // ✅ keeps it
❌ Expecting a non-recursive Flatten to handle nested arrays — it strips exactly one level.
Best Practices
✅ Write infer patterns smallest-first: name only the hole you need
✅ Make the false branch explicit — usually never — so the utility has a defined edge case
✅ Recursion for nested structures: Awaited, deep Flatten, deep Readonly
✅ Put infer utilities in a shared types.ts and give them readable names
✅ Test utilities with type X = MyType<…> assertions (use expect-type in a real codebase)
❌ Don't shadow a pattern you only need once; a concrete type beats clever inference
Interview Questions
Q1. What does infer do?
It's how conditional types bind a type to a variable. Inside
T extends SomePattern, I can writeinfer Xand TypeScript solves forXwhenTmatches the pattern, making it available in the true branch.The classic example:
T extends (...args: any[]) => infer R ? R : never— that'sReturnType, extracted from the pattern rather than hard-coded anywhere.
Q2. Can you reimplement ReturnType<T>? (the flagship question)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;The conditional tries to match
Tagainst a function signature. If it fits,Ris bound to the return type and we returnR. If it doesn't, we returnnever. Theany[]for parameters deliberately accepts any function, since I only want the return side.
Q3. Can infer appear outside a conditional type?
No.
inferis only legal inside theextendsclause of a conditional type, and its scope is that conditional's true branch. You can't declare a free-standinginfervariable, and you can't use the bound type in the false branch.
Q4. Where is infer used in the standard library?
ReturnType<T>,Parameters<T>(the...infer Prest),Awaited<T>(a recursivePromiseunwrap),InstanceType<T>andNonNullable-style utilities all lean on it. The general shape is: pattern-match a container type and bind the part you care about.
Q5. How do you unwrap a nested promise type?
Recursively.
Awaited<T>is roughlytype Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;Each layer strips one
Promise, recursing until the base type is reached.awaitrelies on exactly this:Awaited<Promise<Promise<string>>>isstring.
Senior follow-up: Reimplement Awaited or a deep Flatten.
For
Awaited:type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;For a deep flatten, recurse into the element type as well:
type FlattenDeep<T> = T extends Array<infer U> ? FlattenDeep<U> : T; type A = FlattenDeep<number[][][]>; // numberThe recursion is what makes both work:
inferpeels the container, then the type calls itself on what was inside. The difference between this and a one-levelFlattenis the recursive call in the true branch.
Follow-up Questions
Can you reimplement Parameters<T>?
Swap the hole from the return side to the parameter side:
T extends (...args: infer P) => any ? P : never. The result is the tuple of parameter types —[string, number]for(a: string, b: number) => void.
What happens to infer in the false branch?
It's out of scope. The false branch can't name it, and a failed match simply doesn't bind it. That's why the false branch needs its own concrete type, usually
never.
Can two conditionals share an infer?
No — each
inferbelongs to its own conditional. To combine results you nest conditionals or chain them; there's no "global" type variable.
Does infer have a runtime cost?
No.
inferis compile-time only. TypeScript resolves the conditional during type-checking and emits nothing — the output JavaScript is identical with or without it.
Comparison Table
| Utility | Pattern | Returns |
|---|---|---|
ReturnType<T> | T extends (...args: any[]) => infer R | R — the return type |
Parameters<T> | T extends (...args: infer P) => any | P — parameter tuple |
Awaited<T> | T extends Promise<infer U> ? Awaited<U> | Unwrapped, recursive |
ElementType<T> | T extends (infer E)[] | E — the element type |
Flatten<T> | T extends Array<infer U> | U — one level deep |
FlattenDeep<T> | T extends Array<infer U> ? FlattenDeep<U> | Deepest element |
Code Example
Put it together: a small library of infer utilities plus a real-looking use.
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
type ElementType<T> =
T extends (infer E)[] ? E
: T extends ReadonlyArray<infer E> ? E
: T extends Promise<infer E> ? E
: never;
type Flatten<T> = T extends Array<infer U> ? U : T;
declare function fetchUser(id: string): Promise<{ id: string; name: string }>;
type UserResponse = MyAwaited<MyReturnType<typeof fetchUser>>; // { id: string; name: string }
type FirstUser = ElementType<UserResponse[]>; // { id: string; name: string }
type Id = Flatten<string[]>; // string
console.log('types resolved');
Output:
types resolved
Note
MyAwaited<MyReturnType<typeof fetchUser>> reads as: take the return type of
fetchUser, then unwrap its promise. Nesting infer utilities is composition — the same
way you'd compose functions.
Performance Notes
infer itself costs nothing at runtime — it's erased. The only cost is at compile time:
- Recursive conditionals (
Awaited, deepFlatten) add work per nesting level. Keep them shallow or bounded. - Deeply nested unions in a conditional can make instantiation slow — a pathological
Awaited<HugeUnion>fans out. - TypeScript stops "runaway" recursion with
ts(2589)/ts(2321)errors and an instantiation-depth limit; a circular type alias that never bottoms out fails to compile.
In day-to-day app code this never matters. It becomes relevant only when a shared type utility is applied across a very large codebase, where a couple of milliseconds per file adds up.
Debugging Scenarios
"I get never and I expected a real type." The pattern didn't match. Check the shape:
T extends (infer E)[] won't match a tuple [a, b] written as a tuple — tuples are not
arrays. Match readonly (infer E)[] or readonly [infer A, ...infer Rest] instead.
"Duplicate identifier" on a second infer X. You redeclared the same name in one
pattern. Rename it — each hole needs a distinct variable.
"'infer' declarations are only permitted in the 'extends' clause." You wrote infer
outside a conditional, or in the true branch. It belongs only in the pattern: the left side
of extends.
A recursive type says "excessively deep and possibly infinite". Your recursion never
bottoms out. Add a terminal branch — usually a plain T fallthrough like Awaited's
: T — so the recursion stops when the container is gone.
Quick Revision Notes
infer= pattern-match with a type variable, inside a conditional type- Scope: the conditional's true branch only; false branch can't name it
ReturnType<T>=T extends (...args: any[]) => infer R ? R : neverAwaited<T>= recurse onPromise<infer U>ElementType<T>= match array, then readonly array, then promise, elseneverFlatten<T>=T extends Array<infer U> ? U : T— one level- Recursion is the upgrade: deep flatten,
Awaited,Promise.allinference - One
inferper hole; distinct names in one pattern anyaccepts anything (match),nevermeans no match- Compile-time only — zero runtime output
Key Takeaways
Recap
inferisconstfor types — declare it inside the pattern, and TypeScript binds it- Scope is the conditional's true branch, and only that branch
ReturnType<T>isT extends (...args: any[]) => infer R ? R : never— memorise it, then explain it- Recursion makes
inferunbounded:AwaitedandFlattenDeepare the same idea, recursing on the hole - Multiple holes (
[infer A, infer B]) bind by position - Always define the false branch —
neveris the honest "no match" - Zero runtime cost; compile-time only
- Reimplementing these utilities live is the strongest senior signal there is
Check your understanding
Answer each one out loud. Then rate yourself honestly.
0/7In one sentence, what does infer do — and where is it allowed to appear?
Reimplement ReturnType<T> from memory and explain the any[].
Why must the false branch not reference infer X?
What makes Awaited<T> handle Promise<Promise<boolean>>?
Write ElementType<T> for arrays and ReadonlyArray.
What does Flatten<string[][]> give you, and why isn't it string?
When would you not reach for infer?
What's Next
Lesson 44 — satisfies & as const. The modern pair that keeps precise literal types while checking the shape — modern TypeScript many candidates haven't caught up with yet.