Skip to main content
Advertisement

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

PatternSyntaxUse Case
Extract return typeT extends (...) => infer RFunction return type
Extract parametersT extends (...args: infer P) => anyFunction parameter tuple
Extract array elementT extends (infer E)[]Array/tuple element type
Unwrap PromiseT extends Promise<infer V>Async value type
Constructor parametersT extends new (...args: infer P) => anyClass constructor parameters
Recursive inferinfer + recursive conditional typesDeep structure transformation
Covariant positionReturn values, object property valuesMerged as a union
Contravariant positionFunction parametersMerged as an intersection

In the next chapter we take a deep look at TypeScript's advanced built-in utility typesAwaited, ConstructorParameters, InstanceType, ThisType, and more — and explore how to combine them in real-world patterns.

Advertisement