Ch. 2 · TypeScript

TypeScript Infer for Type Extraction

How infer pulls element, return and parameter types out of other types, why readonly arrays don't match T[], and how to parse route strings.

~5 min readintermediateupdated Oct 7, 2026

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>;            // never
TypeScript

Read 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";
TypeScript
inf3.ts(4,7): error TS2322: Type '"admin"' is not assignable to type 'never'.
Text

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"] };
TypeScript
inf1.ts(16,49): error TS2322: Type '"user"' is not assignable to type '"admin"'.
Text

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";
TypeScript
inf1.ts(34,7): error TS2322: Type 'string' is not assignable to type 'number'.
Text

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 }
TypeScript

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";
TypeScript
inf1.ts(25,7): error TS2322: Type '"userId"' is not assignable to type '"orgId" | "repoId"'.
Text

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;
TypeScript

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.

Further reading

More in TypeScript

read ✓TypeScript · mid

TypeScript Template Literal Types

Build string types from unions, parse route parameters with infer, see what ${number} really accepts, and know where these types stop helping.

~6 min readread →
read ✓TypeScript · hard

TypeScript Key Remapping in Mapped Types

Rename keys with as clauses, drop keys by mapping to never, map over a union of events, and spot the string & K and optional-method traps.

~6 min readread →
read ✓TypeScript · mid

TypeScript Conditional Types and Distribution

How T extends U ? X : Y picks a type, why it splits unions apart member by member, and how wrapping both sides in brackets turns that off.

~6 min readread →
esc