Sometimes the type you need already exists, buried inside another type: the element type of an array, the resolved value of a promise, the second parameter of a callback from a library you don’t control. infer lets you pattern-match a type and pull that piece out. It powers ReturnType, Parameters and Awaited in the standard library, and writing a small infer helper on a whiteboard is a common intermediate interview task.
Pattern matching with a placeholder
infer only works inside the extends clause of a conditional type. You write the shape you expect and put infer Name where the unknown piece sits:
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<readonly [1, "x"]>; // 1 | "x"
type C = ElementOf<number>; // neverRead it as: “if T is some array, call its element type E and give me E; otherwise give me never.” TypeScript tries to match T against the pattern and, if it can, binds E to whatever was in that position. A tuple matches too, and its element type is the union of its members.
The false branch matters more than people think. never is a sensible default for “this wasn’t an array” when the result feeds into other types, because never vanishes from unions. But it can also hide mistakes, which is the next section.
Why readonly arrays fall through
Drop the readonly from the pattern and try it with a constant list:
type ElementOfMutable<T> = T extends (infer E)[] ? E : never;
const ROLES = ["admin", "editor"] as const;
type Role = ElementOfMutable<typeof ROLES>;
const r: Role = "admin";inf3.ts(4,7): error TS2322: Type '"admin"' is not assignable to type 'never'.as const produces readonly ["admin", "editor"]. A readonly array has no push, so it isn’t assignable to a mutable E[], the pattern doesn’t match, and Role silently becomes never. You only find out when something downstream refuses every value. Matching against readonly (infer E)[] accepts both kinds, because a mutable array is assignable to a readonly one. This is the single most common bug in hand-written extraction types, and mentioning it unprompted makes a strong impression.
Pulling types out of functions
Function types have two obvious places to infer from: the return position and the parameter list.
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type FirstArg<F> = F extends (first: infer A, ...rest: any[]) => any ? A : never;
async function fetchUser(id: string, opts?: { cache: boolean }) {
return { id, name: "Ada", roles: ["admin"] as const };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
type Arg = FirstArg<typeof fetchUser>; // string
const u: User = { id: "1", name: "Ada", roles: ["user"] };inf1.ts(16,49): error TS2322: Type '"user"' is not assignable to type '"admin"'.ReturnType<typeof fetchUser> is Promise<{ ... }>, and Awaited unwraps it. The useful habit here is to derive types from the function that produces the data instead of declaring an interface and hoping the two stay in sync. If someone changes fetchUser to return an extra field, User updates with it. The literal "admin" survived because of the as const in the function body, which is why the assignment of "user" fails.
The any[] in these helpers isn’t laziness. A function with parameters (id: string) is assignable to (...args: any[]) => R, but because parameters are checked contravariantly, it would not be assignable to (...args: unknown[]) => R. The standard library’s ReturnType uses any for the same reason.
Overloads only show their last signature
type Overloaded = { (x: string): string; (x: number): number };
type OR = ReturnType<Overloaded>;
const orr: OR = "s";inf1.ts(34,7): error TS2322: Type 'string' is not assignable to type 'number'.When you infer from an overloaded function type, TypeScript uses the last overload, which is usually the most general one. There’s no built-in way to get the return type for a specific overload. If you hit this with a library function, either wrap the call in your own function with a single signature, or declare the type you need explicitly.
Same name twice: union or intersection
If one infer name appears in more than one position, the result depends on the position:
type Co<T> = T extends { a: infer U; b: infer U } ? U : never;
type X = Co<{ a: string; b: number }>; // string | number
type Contra<T> = T extends { a: (v: infer U) => void; b: (v: infer U) => void } ? U : never;
type Y = Contra<{ a: (v: { id: string }) => void; b: (v: { name: string }) => void }>;
// { id: string } & { name: string }In ordinary (covariant) positions, the candidates are combined into a union. In parameter (contravariant) positions they’re combined into an intersection, because a value that’s safe to pass to both callbacks has to satisfy both parameter types. The well-known UnionToIntersection helper exploits exactly this. It’s an advanced follow-up, but it shows that infer follows the same variance rules as everything else.
Inferring from template literals
infer works inside template literal types too, which lets you parse string literal types at compile time. A router can extract parameter names from its path pattern:
type RouteParams<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: S extends `${string}:${infer Param}`
? Param
: never;
type P = RouteParams<"/orgs/:orgId/repos/:repoId">; // "orgId" | "repoId"
const p: P = "userId";inf1.ts(25,7): error TS2322: Type '"userId"' is not assignable to type '"orgId" | "repoId"'.The first branch matches a parameter followed by more path, takes the name, and recurses on the rest. The second branch handles a final parameter with nothing after it. Each infer placeholder matches as little as possible up to the next literal character, which is why Param stops at the /. Real routers type their params objects this way.
Since TypeScript 4.7 you can constrain an inferred type inline. T extends `${infer N extends number}` ? N : never turns the string "42" into the number literal 42, and a non-numeric string fails the match.
When indexed access is simpler
infer isn’t always the right tool. If the piece you want sits at a known property or index, an indexed access type says it more directly. The element type of an array is T[number], so (typeof ROLES)[number] gives "admin" | "editor" without any conditional type at all, and it works for readonly arrays automatically. The type of a response’s data field is Response["data"]. Reach for infer when the thing you want has no name to index by: a return type, a parameter, a promise’s value, or a slice of a string. Interviewers sometimes ask for ElementOf precisely to see whether you notice that T[number] already does the job.
Interview practice
Write PromiseValue<T> that gives the resolved value of a promise and leaves non-promise types alone. Then explain how it differs from the built-in Awaited, and when that difference matters.
Answer
type PromiseValue<T> = T extends Promise<infer V> ? V : T;For Promise<number> it matches, binds V to number and returns it. For string it doesn’t match and returns T unchanged, which is the “leave it alone” part. Because T is a naked type parameter, it also distributes, so PromiseValue<Promise<number> | string> is number | string.
It differs from Awaited in two ways. It unwraps only one level, so PromiseValue<Promise<Promise<number>>> is Promise<number>, while Awaited recurses to number, matching what await does at runtime. And it only matches real Promise types, while Awaited matches any object with a callable then method, which covers custom thenables from older libraries. So for typing the result of an await, Awaited is the correct choice; a one-level helper is only right when you specifically want to peel a single layer, for example when modelling a function that returns a promise of a promise on purpose.