7.1 The infer Keyword
When you first learn TypeScript you specify types explicitly, but advanced type programming requires the ability to infer and extract types. The infer keyword tells TypeScript inside a conditional type: "capture the type at this position into a variable so I can use it later." Think of it like pulling a puzzle piece out and labeling it — you can isolate exactly the part of a complex type structure you care about.
What is infer
infer is a special keyword that can only be used inside an extends conditional type clause. Similar in concept to pattern matching, when a given type matches a pattern, infer captures the type at a specific position within that pattern into a new type variable.
// Basic syntax
type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// How R gets inferred:
// If T = () => string
// () => string extends (...args: any[]) => infer R
// => R is inferred as string
// Result: string
infer R instructs TypeScript to "store the type at the return position into a type variable called R." R is only available in the true branch; it does not exist in the false branch.
Core Concepts
The Relationship Between Conditional Types and infer
infer must always be used inside a conditional type (T extends ... ? A : B). It cannot be used on its own.
// Correct usage
type Correct<T> = T extends Promise<infer V> ? V : never;
// Incorrect usage — compile error
// type Wrong<T> = infer V; // ❌
The Meaning of infer's Position
The type that gets inferred depends on where infer is placed.
// Function return type
type ReturnT<T> = T extends (...args: any[]) => infer R ? R : never;
// Function parameter types (inferred as a tuple)
type ParamsT<T> = T extends (...args: infer P) => any ? P : never;
// Array element type
type ElementT<T> = T extends (infer E)[] ? E : never;
// Promise inner type
type PromiseT<T> = T extends Promise<infer V> ? V : never;
Implementing ReturnType, Parameters, and ConstructorParameters Yourself
Let's build our own versions of TypeScript's built-in utility types to see how they work internally.
// Implementing ReturnType<T> ourselves
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
function add(a: number, b: number): number {
return a + b;
}
type AddReturn = MyReturnType<typeof add>; // number
// Implementing Parameters<T> ourselves
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type AddParams = MyParameters<typeof add>; // [a: number, b: number]
// Implementing ConstructorParameters<T> ourselves
type MyConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
class User {
constructor(
public name: string,
public age: number,
public email: string
) {}
}
type UserCtorParams = MyConstructorParameters<typeof User>;
// [name: string, age: number, email: string]
// Implementing InstanceType<T> ourselves
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer I ? I : never;
type UserInstance = MyInstanceType<typeof User>; // User
Extracting Array Element Types
This pattern extracts the element type from an array type.
// One-dimensional array element type
type ArrayElement<T> = T extends (infer Item)[] ? Item : never;
type StringItem = ArrayElement<string[]>; // string
type NumberItem = ArrayElement<number[]>; // number
type MixedItem = ArrayElement<(string | number)[]>; // string | number
// Handling readonly arrays too
type ReadonlyArrayElement<T> =
T extends readonly (infer Item)[] ? Item : never;
type ROItem = ReadonlyArrayElement<readonly string[]>; // string
// Extracting the deepest element type from nested arrays (recursive)
type DeepArrayElement<T> =
T extends (infer Item)[]
? DeepArrayElement<Item>
: T;
type Nested = DeepArrayElement<string[][][]>; // string
type Flat = DeepArrayElement<number[]>; // number
type Scalar = DeepArrayElement<boolean>; // boolean
// Extracting a type at a specific position from a tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type TupleFirst = First<[string, number, boolean]>; // string
type TupleLast = Last<[string, number, boolean]>; // boolean
type EmptyFirst = First<[]>; // never
Extracting the First and Last Function Parameters
// First parameter type
type FirstParameter<T extends (...args: any) => any> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
// Last parameter type
type LastParameter<T extends (...args: any) => any> =
T extends (...args: infer P) => any
? P extends [...any[], infer L]
? L
: never
: never;
// Tail parameter types (everything except the first)
type TailParameters<T extends (...args: any) => any> =
T extends (first: any, ...rest: infer R) => any ? R : never;
// Test
function greet(name: string, age: number, city: string): string {
return `${name}(${age}) from ${city}`;
}
type GreetFirst = FirstParameter<typeof greet>; // string
type GreetLast = LastParameter<typeof greet>; // string
type GreetTail = TailParameters<typeof greet>; // [age: number, city: string]
// Practical use: parameter separation for currying
type Curry<T extends (...args: any) => any> =
Parameters<T> extends [infer Head, ...infer Tail]
? Tail extends []
? T
: (arg: Head) => Curry<(...args: Tail) => ReturnType<T>>
: never;
Promise Unwrapping — How Awaited Works
Let's implement the principle behind Awaited<T>, which was built into TypeScript 4.5.
// Simple version: unwrap one level
type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
type S1 = UnwrapPromise<Promise<string>>; // string
type S2 = UnwrapPromise<string>; // string (returned as-is if not a Promise)
// Recursive version: fully unwraps even nested Promises
type DeepUnwrapPromise<T> =
T extends Promise<infer V>
? DeepUnwrapPromise<V>
: T;
type Nested1 = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>; // string
type Nested2 = DeepUnwrapPromise<Promise<number[]>>; // number[]
// The actual implementation of Awaited<T> (reference: TypeScript lib.es5.d.ts)
type MyAwaited<T> =
T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
? F extends (value: infer V, ...args: infer _) => any
? MyAwaited<V>
: never
: T;
// Also supports PromiseLike (only needs a then method)
type A1 = MyAwaited<Promise<string>>; // string
type A2 = MyAwaited<{ then: (f: (v: number) => any) => any }>; // number
type A3 = MyAwaited<string>; // string
// Extract the actual value type from an async function's return type
async function fetchUser(): Promise<{ id: number; name: string }> {
return { id: 1, name: "Alice" };
}
type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
Combining Recursion with infer
Combining infer with recursive types enables extremely powerful type transformations.
// Convert a tuple to a union type
type TupleToUnion<T extends any[]> =
T extends [infer Head, ...infer Tail]
? Head | TupleToUnion<Tail>
: never;
type Union1 = TupleToUnion<[string, number, boolean]>;
// string | number | boolean
// Reverse a tuple
type Reverse<T extends any[]> =
T extends [infer Head, ...infer Tail]
? [...Reverse<Tail>, Head]
: [];
type Rev1 = Reverse<[1, 2, 3]>; // [3, 2, 1]
type Rev2 = Reverse<[string, number]>; // [number, string]
// Infer the final return type of a function chain
type ChainReturn<T> =
T extends (...args: any[]) => infer R
? R extends (...args: any[]) => any
? ChainReturn<R>
: R
: T;
declare function makeAdder(x: number): (y: number) => string;
type AdderResult = ChainReturn<typeof makeAdder>; // string
// Extract the type at a deep key path from a nested object
type GetPath<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetPath<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
app: {
name: string;
version: string;
};
}
type DbHost = GetPath<Config, "database.host">; // string
type DbPort = GetPath<Config, "database.port">; // number
type DbUser = GetPath<Config, "database.credentials.username">; // string
type AppName = GetPath<Config, "app.name">; // string
Practical Example 1: Middleware Chain Type Inference
Automatically infer context types from an Express-style middleware chain.
// Middleware type definition
type Middleware<TIn, TOut> = (
ctx: TIn,
next: (ctx: TOut) => void
) => void;
// Infer the final context type from a middleware chain
type ExtractMiddlewareOutput<T> =
T extends Middleware<any, infer Out> ? Out : never;
type ExtractMiddlewareInput<T> =
T extends Middleware<infer In, any> ? In : never;
// A middleware chain that progressively extends the context
interface BaseCtx {
requestId: string;
timestamp: Date;
}
interface AuthCtx extends BaseCtx {
userId: string;
roles: string[];
}
interface ValidatedCtx extends AuthCtx {
body: unknown;
params: Record<string, string>;
}
// The type of each middleware is automatically inferred
const authMiddleware: Middleware<BaseCtx, AuthCtx> = (ctx, next) => {
next({ ...ctx, userId: "user-123", roles: ["admin"] });
};
const validationMiddleware: Middleware<AuthCtx, ValidatedCtx> = (ctx, next) => {
next({ ...ctx, body: {}, params: {} });
};
type AuthOutput = ExtractMiddlewareOutput<typeof authMiddleware>; // AuthCtx
type ValidInput = ExtractMiddlewareInput<typeof validationMiddleware>; // AuthCtx
// Pipeline type — the final output type of an array of middlewares
type PipelineOutput<T extends Middleware<any, any>[]> =
T extends [...any[], infer Last]
? ExtractMiddlewareOutput<Last>
: never;
type Pipeline = [typeof authMiddleware, typeof validationMiddleware];
type FinalCtx = PipelineOutput<Pipeline>; // ValidatedCtx
// Actual pipeline runner
function createPipeline<T extends BaseCtx>(
middlewares: Middleware<any, any>[]
): (ctx: T) => void {
return (ctx: T) => {
let index = 0;
const run = (currentCtx: any) => {
if (index < middlewares.length) {
const middleware = middlewares[index++];
middleware(currentCtx, run);
}
};
run(ctx);
};
}
Practical Example 2: Inferring Return Types of Composed Functions
Safely infer types in functional programming compose / pipe patterns.
// Infer the return type of a single function composition
type ComposedReturn<F extends (...args: any) => any, G extends (...args: any) => any> =
ReturnType<F> extends Parameters<G>[0]
? (...args: Parameters<F>) => ReturnType<G>
: never;
// pipe: executes left to right
function pipe<A, B>(
f: (a: A) => B
): (a: A) => B;
function pipe<A, B, C>(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C;
function pipe<A, B, C, D>(
f: (a: A) => B,
g: (b: B) => C,
h: (c: C) => D
): (a: A) => D;
function pipe(...fns: Function[]) {
return (x: any) => fns.reduce((acc, fn) => fn(acc), x);
}
const transform = pipe(
(n: number) => n.toString(),
(s: string) => s.length,
(n: number) => n > 5
);
// Type: (a: number) => boolean
// Inferring the return type of event handlers
type EventHandler<T extends Event> = (event: T) => void;
type ExtractEventType<H> = H extends EventHandler<infer E> ? E : never;
type ClickHandlerEvent = ExtractEventType<EventHandler<MouseEvent>>; // MouseEvent
// Inferring API response types
type ApiFunction = (...args: any[]) => Promise<any>;
type ApiResponse<T extends ApiFunction> =
Awaited<ReturnType<T>>;
async function getUser(id: string): Promise<{ id: string; name: string; email: string }> {
// API call
return { id, name: "Alice", email: "alice@example.com" };
}
async function getUserList(): Promise<Array<{ id: string; name: string }>> {
return [];
}
type SingleUser = ApiResponse<typeof getUser>;
// { id: string; name: string; email: string }
type UserList = ApiResponse<typeof getUserList>;
// { id: string; name: string }[]
Pro Tips
infer Position Rules: Covariant vs Contravariant
When multiple infer references use the same type variable, the result differs depending on whether the position is covariant or contravariant.
// Covariant position: merged as a union type
type CovariantInfer<T> =
T extends { a: infer U; b: infer U } ? U : never;
type C1 = CovariantInfer<{ a: string; b: number }>;
// string | number ← union
// Contravariant position: merged as an intersection type
type ContravariantInfer<T> =
T extends {
fn: (x: infer U) => void;
fn2: (x: infer U) => void;
}
? U
: never;
type F1 = ContravariantInfer<{
fn: (x: string) => void;
fn2: (x: number) => void;
}>;
// string & number = never ← intersection
Using Multiple infer Variables at Once
You can use multiple infer variables simultaneously inside a single conditional type.
// Extract both the first parameter and return type of a function at once
type FirstParamAndReturn<T> =
T extends (first: infer P, ...rest: any[]) => infer R
? { param: P; return: R }
: never;
function process(id: number, data: string): boolean {
return true;
}
type ProcessInfo = FirstParamAndReturn<typeof process>;
// { param: number; return: boolean }
// Extract key-value pairs from an object type at once
type KeyValuePair<T> =
T extends { [K in infer Key extends string]: infer Val }
? { key: Key; value: Val }
: never;
// Map type transformation utility
type MapTuple<T extends [any, any][]> = {
[K in T[number] as K[0]]: K[1];
};
type MyMap = MapTuple<[
["name", string],
["age", number],
["active", boolean]
]>;
// { name: string; age: number; active: boolean }
Debugging infer
// A debug utility for examining what infer has inferred
type Inspect<T> = T extends infer U ? U : never;
// Inspect intermediate types in complex type expressions
type ComplexType = Inspect<
ReturnType<typeof JSON.parse>
>; // any
// Break a type down when it doesn't match expectations
type Debug<T, Label extends string> = {
[K in Label]: T
};
type CheckResult = Debug<
ReturnType<typeof fetch>,
"FetchResult"
>; // { FetchResult: Promise<Response> }
Summary
| Pattern | Syntax | Use Case |
|---|---|---|
| Extract return type | T extends (...) => infer R | Function return type |
| Extract parameters | T extends (...args: infer P) => any | Function parameter tuple |
| Extract array element | T extends (infer E)[] | Array/tuple element type |
| Unwrap Promise | T extends Promise<infer V> | Async value type |
| Constructor parameters | T extends new (...args: infer P) => any | Class constructor parameters |
| Recursive infer | infer + recursive conditional types | Deep structure transformation |
| Covariant position | Return values, object property values | Merged as a union |
| Contravariant position | Function parameters | Merged as an intersection |
In the next chapter we take a deep look at TypeScript's advanced built-in utility types — Awaited, ConstructorParameters, InstanceType, ThisType, and more — and explore how to combine them in real-world patterns.