The infer Keyword in TypeScript


The infer keyword is TypeScript's way of capturing a type from within a conditional check. Think of it like a regex capture group — just as /(\d+)/ pulls a number out of a string match, infer R pulls a type out of a type match. It can only appear inside the extends clause of a conditional type, and it gives the captured type a name you can use in the true branch. In this post, you'll learn how infer works, how to use it to extract return types, parameter types, array element types, and Promise values, and how standard utility types like ReturnType and Parameters are built with it.
Key Constraint
infer can only appear on the right-hand side of an extends clause in a
conditional type. It declares a type variable that TypeScript will fill in by
pattern matching against the input type.
What Is the infer Keyword
Without infer, conditional types can only check whether a relationship holds — they can't extract the type involved in that relationship. The infer keyword adds extraction:
// Without infer — checks if T is a function, returns boolean-like types
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
// With infer — checks if T is a function AND captures what it returns
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;In the second form, if T is a function, TypeScript pattern-matches its return type and binds it to R. The R variable is then available in the true branch (R) and can be returned as the resolved type.
Inferring Return Types
The most common use of infer is extracting a function's return type. TypeScript's built-in ReturnType<T> does exactly this:
type ReturnType<T extends (...args: any) => any> = T extends (
...args: any
) => infer R
? R
: any;
function getUser() {
return { id: 1, name: "Alice", role: "admin" as const };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; role: "admin" }This is incredibly useful when you want to derive the return type of a function you don't control — from a third-party library, for instance — without importing a type that might not be exported.
// Library doesn't export this type, but you can derive it
import { createConnection } from "some-database-library";
type Connection = ReturnType<typeof createConnection>;Inferring Parameter Types
You can infer the parameter types of a function using infer on the arguments side:
type Parameters<T extends (...args: any) => any> = T extends (
...args: infer P
) => any
? P
: never;
function createPost(title: string, authorId: number, published: boolean) {
// ...
}
type PostParams = Parameters<typeof createPost>;
// [title: string, authorId: number, published: boolean]
// Extract just the first parameter
type FirstParam<T extends (...args: any) => any> = T extends (
first: infer F,
...rest: any[]
) => any
? F
: never;
type TitleType = FirstParam<typeof createPost>; // stringParameters Returns a Tuple
Parameters<T> returns a tuple type representing all the argument types in order. You can index into it with Parameters<T>[0] to get just the first parameter's type.
Inferring Array Element Types
infer works well for extracting what's inside container types:
type ElementType<T> = T extends (infer E)[] ? E : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[][]>; // number[] — unwraps one level
type C = ElementType<boolean>; // never — not an array
// For readonly arrays too
type ReadonlyElementType<T> = T extends readonly (infer E)[] ? E : never;
type D = ReadonlyElementType<readonly string[]>; // stringYou can make it recursive to unwrap nested arrays:
type DeepElementType<T> = T extends (infer E)[] ? DeepElementType<E> : T;
type E = DeepElementType<string[][][]>; // stringInferring Promise Unwrapping
infer shines when working with async code. TypeScript's Awaited<T> utility type is built on this pattern:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number[]>>; // number[]
type C = UnwrapPromise<string>; // string — not a Promise, returned as-is
// Recursive version — handles Promise<Promise<...>>
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type D = DeepAwaited<Promise<Promise<string>>>; // stringTypeScript 4.5 introduced the built-in Awaited<T> utility type which handles more edge cases than a simple recursive unwrap. Prefer the built-in over rolling your own.
Real-World Usage
Here's a pattern for building a type-safe event system where event handler return types are extracted automatically:
type EventHandlers = {
click: (x: number, y: number) => void;
keydown: (key: string, modifiers: string[]) => boolean;
resize: (width: number, height: number) => { handled: boolean };
};
// Extract return type for a specific event
type HandlerReturn<T extends keyof EventHandlers> = ReturnType<
EventHandlers[T]
>;
type ClickReturn = HandlerReturn<"click">; // void
type KeyReturn = HandlerReturn<"keydown">; // boolean
type ResizeReturn = HandlerReturn<"resize">; // { handled: boolean }
// Extract first parameter type for a specific event
type FirstArg<T extends (...args: any[]) => any> = T extends (
first: infer F,
...rest: any[]
) => any
? F
: never;
type ClickX = FirstArg<EventHandlers["click"]>; // number
type KeyName = FirstArg<EventHandlers["keydown"]>; // stringAnother common pattern extracts the type inside a custom wrapper:
type Boxed<T> = { value: T; tag: string };
// Unbox a Boxed<T> to get T
type Unbox<T> = T extends Boxed<infer U> ? U : T;
type NumberBox = Boxed<number>;
type Unboxed = Unbox<NumberBox>; // numberBuild Your Own Utility Types
Once you understand infer, every TypeScript utility type becomes readable.
Open node_modules/typescript/lib/lib.es5.d.ts and look at how ReturnType,
Parameters, InstanceType, and ConstructorParameters are all built with
the same pattern.
Best Practices
Name your inferred variables descriptively. infer R is fine in a short type, but infer ReturnValue or infer ElementType makes complex types much easier to read.
Handle the false branch intentionally. Returning never from the false branch is common, but sometimes you want to return T unchanged (passthrough) or a specific fallback type.
Don't over-infer. If TypeScript already exposes the type you need (like a library's exported type), use that directly rather than inferring it. Save infer for when types aren't directly accessible.
Multiple infer positions work. You can have multiple infer declarations in one extends clause:
type SplitFunction<T> = T extends (...args: infer Args) => infer Return
? { args: Args; return: Return }
: never;
type Info = SplitFunction<(x: number, y: string) => boolean>;
// { args: [x: number, y: string]; return: boolean }Anti-Pattern
Don't use infer outside of conditional types — it's only valid inside the
extends clause. Trying to use it elsewhere is a syntax error.
What Is Next
With conditional types and infer in your toolkit, you're ready for mapped types — TypeScript's way of transforming every property in a type according to a rule. Mapped types are how Partial<T>, Required<T>, and Readonly<T> work, and they pair beautifully with conditional types.
Key Takeaways
inferonly works inside theextendsclause of a conditional type — it declares a type variable TypeScript fills by pattern matchingReturnType<T>andParameters<T>are built-in utilities that useinferunder the hood- You can infer from function arguments, return positions, array element positions, and generic type arguments
- Multiple
inferdeclarations in one clause are allowed and useful - Name inferred type variables descriptively to keep complex conditional types readable
You now understand the engine behind TypeScript's most powerful utility types.
infer is the bridge between structural type checking and type extraction —
and it unlocks a whole category of patterns you couldn't write any other way.