TypeScript infer: The Keyword That Unlocks Advanced Type Extraction
If you've ever wanted to pluck the return type out of a function signature, grab the element type from an array, or unwrap a Promise — without maintaining a parallel type definition — you're looking for TypeScript's infer keyword.
infer is the difference between writing this:
type UserData = { id: number; name: string; email: string };
async function fetchUser(id: number): Promise<UserData> {
const res = await fetch(`/users/${id}`);
return res.json();
}
// Manual duplication — if fetchUser changes, this breaks silently
type FetchedUser = { id: number; name: string; email: string };
And writing this:
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
type Unwrap<T> = T extends Promise<infer V> ? V : never;
// Extracted automatically — always in sync
type FetchedUser = Unwrap<Return<typeof fetchUser>>;
// ^? { id: number; name: string; email: string }
No duplication. No drift. One source of truth.
What Actually Is infer?
infer is a keyword that only works inside the extends clause of a conditional type. It declares a type variable that TypeScript fills in from context — think of it as pattern matching for types.
type SomeConditional<T> = T extends SomePattern<infer Extracted>
? /* use Extracted here */
: /* fallback */;
The rule: you can only use an inferred type variable inside the true branch (?) of the conditional. TypeScript figures out what Extracted should be by matching T against SomePattern.
Pattern 1: Extract Return Types (The "Hello World")
Every TypeScript developer has seen ReturnType<T>. Here's how it works:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(name: string): string {
return `Hello, ${name}!`;
}
function fetchConfig(): { theme: string; version: number } {
return { theme: "dark", version: 2 };
}
type GreetResult = MyReturnType<typeof greet>;
// ^? string
type Config = MyReturnType<typeof fetchConfig>;
// ^? { theme: string; version: number }
The infer R tells TypeScript: "match the return type of this function signature and assign it to R". If T is a function type, R becomes whatever type it returns. If T isn't a function, the never branch kicks in.
⚠️ Only works with typeof on the function, not a call. Pass the type of the function, not its result:
type Wrong = MyReturnType<fetchConfig>; // ❌ 'fetchConfig' refers to a value
type Right = MyReturnType<typeof fetchConfig>; // ✅ Works
Pattern 2: Unwrap Arrays
Same pattern, different inference target:
type ArrayElement<T> = T extends Array<infer E> ? E : never;
type A = ArrayElement<string[]>; // string
type B = ArrayElement<number[]>; // number
type C = ArrayElement<({ name: string; age: number } | { name: string; admin: boolean })[]>;
// ^? { name: string; age: number } | { name: string; admin: boolean }
type D = ArrayElement<number>; // never (not an array)
This works with ReadonlyArray too:
type Element<T> = T extends readonly (infer E)[] ? E : never;
type E = Element<readonly string[]>; // string
Pattern 3: Unwrap Promises
Probably the most practical pattern in async TypeScript codebases:
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;
// Single level
type Inner = Awaited<Promise<string>>;
// ^? string
// Recursive unwrapping
type Deep = Awaited<Promise<Promise<Promise<number>>>>;
// ^? number
// Non-promise passes through
type Plain = Awaited<string>;
// ^? string
Notice the recursive call to Awaited<V> — this lets it unwrap nested promises. With every level, TypeScript peels off one Promise<> wrapper and infers the inner type, then recursively processes it.
This is so useful that TypeScript 4.5 shipped it as the built-in Awaited<T> type.
Pattern 4: Extract Function Parameters
You can infer more than just return types. The arguments tuple is fair game:
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function createUser(
name: string,
age: number,
role: "admin" | "user"
): void {
// ...
}
type CreateUserArgs = Parameters<typeof createUser>;
// ^? [name: string, age: number, role: "admin" | "user"]
// Need just the first parameter? Narrow the inference:
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Name = FirstParam<typeof createUser>;
// ^? string
// Need the second parameter?
type SecondParam<T> = T extends (first: any, second: infer S, ...rest: any[]) => any ? S : never;
type Age = SecondParam<typeof createUser>;
// ^? number
This is how TypeScript's built-in Parameters<T> utility type works under the hood.
Pattern 5: Extract from Constructor Signatures
Same idea, but for classes and constructable objects:
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;
class Database {
constructor(private url: string, private poolSize: number) {}
connect() { /* ... */ }
}
type Db = InstanceType<typeof Database>;
// ^? Database
// Constructor parameters:
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
type DbParams = ConstructorParams<typeof Database>;
// ^? [url: string, poolSize: number]
Built-in InstanceType<T> and ConstructorParameters<T> use exactly this pattern.
Pattern 6: Multiple Inferences in One Check
You're not limited to a single infer — TypeScript can extract multiple pieces from one type:
type ExtractFunctionSignature<T> = T extends (
...args: infer P
) => infer R
? { params: P; returnType: R }
: never;
function calculate(x: number, y: number): number {
return x + y;
}
type Sig = ExtractFunctionSignature<typeof calculate>;
// ^? { params: [x: number, y: number]; returnType: number }
This is incredibly useful for metaprogramming — wrapping functions, building middleware, or creating type-safe event systems.
Pattern 7: String Template Literal Extraction
Combined with template literal types (TypeScript 4.1+), infer becomes even more powerful:
// Extract route parameter names from a URL pattern
type ExtractRouteParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {};
type UserRoute = ExtractRouteParams<"/users/:id">;
// ^? { id: string }
type PostRoute = ExtractRouteParams<"/posts/:postId/comments/:commentId">;
// ^? { postId: string; commentId: string }
TypeScript recursively infers parameter names from the string pattern. Each :paramName segment gets extracted as a string-typed property. Frameworks like tRPC and Hono use variations of this pattern.
Pattern 8: Discriminated Union Resolution
Need to resolve a union to the correct member based on its discriminant? infer handles it:
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; ctrlKey: boolean };
focus: { element: HTMLElement };
};
// Extract the payload for a specific event type
type EventPayload<T extends keyof EventMap> = EventMap[T];
// Simple indexed access
// Or: extract from a union of { type: string, payload: infer P }
type ExtractByType<T extends { type: string }, Discriminant extends T['type']> =
T extends { type: Discriminant; payload: infer P } ? P : never;
type UserEvent =
| { type: "created"; payload: { id: number } }
| { type: "updated"; payload: { id: number; changes: string[] } }
| { type: "deleted"; payload: { id: number } };
type CreatedPayload = ExtractByType<UserEvent, "created">;
// ^? { id: number }
When NOT to Use infer
infer is powerful, but it isn't the right tool for every problem:
| Problem | Use infer? |
Better alternative |
|---|---|---|
| Extract return type of a specific function | ✅ Yes |
ReturnType<typeof fn> (uses infer internally) |
| Unwrap Promise value | ✅ Yes |
Awaited<T> (uses infer internally) |
| Extract parameters of a callback | ✅ Yes | Parameters<T> |
| Make a property optional | ❌ No |
Partial<T>, mapped types |
| Pick specific keys from an object | ❌ No | Pick<T, K> |
| Transform object values | ❌ No | Mapped types { [K in keyof T]: ... }
|
| Enforce minimum/maximum length tuples | ❌ No | Variadic tuple types [T, ...T[]]
|
If you find yourself nesting infer more than 3 levels deep, step back. There's often a simpler approach with mapped types, indexed access types, or union types.
Common Gotchas
1. infer only works in conditional types. This is the #1 mistake:
// ❌ This is NOT valid:
type Extract<T, infer R> = R;
// ✅ This is:
type Extract<T> = T extends SomeType<infer R> ? R : never;
2. Distributive conditional types can surprise you. When you pass a union to a conditional type with a naked type parameter, each union member is checked independently:
type Element<T> = T extends (infer E)[] ? E : never;
type Result = Element<string[] | number[]>;
// ^? string | number — distributed over the union
3. Recursive infer has depth limits. TypeScript caps recursive conditional type instantiation at 50 levels by default (configurable with --recursiveTypeDepth in TypeScript 5.6+). If you hit instantiation depth errors, restructure your types instead of bumping the limit. Don't use infer recursion for deeply nested object structures — mapped types handle that better.
4. infer can't "invent" types. It can only extract what's already structurally present:
// ❌ This won't infer 'bar' because there's nothing providing that shape
type ExtractBar<T> = T extends { bar: infer R } ? R : never;
type Test = ExtractBar<{ foo: string }>; // never (correctly!)
The Takeaway
infer is TypeScript's pattern-matching tool for types. It's what makes advanced utility types (ReturnType, Parameters, Awaited, ConstructorParameters) possible, and it's the foundation for type-level metaprogramming in modern TypeScript.
The mental model is simple: declare a type variable inside the pattern match, and TypeScript fills it in.
Start with the basic patterns — return types, array elements, Promise values — and work up. Once infer clicks, you'll start seeing type extraction opportunities everywhere. And you'll stop manually duplicating types that the compiler already knows.
Article by Kai Thorne. Write type-safe code, not type-loud code.
Top comments (0)