TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.
What you'll learn
- ✓How conditional types work and read
- ✓The infer keyword for extracting type parts
- ✓Distributive behavior over unions
- ✓Composing conditional and mapped types
- ✓When NOT to reach for them
Prerequisites
- •Comfortable with JS
- •Basic TypeScript generics
What and Why
A conditional type is a type-level if. It picks between two branches based on whether a type extends another. The syntax is T extends U ? X : Y. With this single primitive, TypeScript can express patterns that adapt to whatever shape callers pass.
Conditionals shine when you build libraries. A type guard might return different shapes depending on the input, or a query builder might infer the result type from the columns selected. Without conditionals, you would need a separate overload for every shape.
Mental Model
Read T extends U ? X : Y aloud as “if T is assignable to U, then X, otherwise Y.” The extends check is structural, not nominal: TypeScript compares the shapes, not the names.
T extends U ?
|--> yes : produce X
|--> no : produce Y When T is a naked type parameter and you give it a union, the conditional distributes. That means it runs the check for each member of the union and joins the results.
Hands-on Example
A classic case is extracting non-nullable values:
type NonNullish<T> = T extends null | undefined ? never : T;
type A = NonNullish<string | null | number>;
// string | number
Because T is naked, the union splits. We check each member: string is not null, so it stays; null matches, returning never; number stays. The final union strips never.
Use infer to pull pieces out of a structural type. Get the return type of any function:
type ReturnOf<F> = F extends (...args: any[]) => infer R ? R : never;
type X = ReturnOf<() => number>; // number
type Y = ReturnOf<(a: string) => Promise<User>>; // Promise<User>
infer R introduces a fresh type variable. TypeScript fills it in when matching against the pattern.
F = (a: string) => Promise<User>
pattern: (...args) => infer R
match! R is bound to Promise<User> Combine conditionals with mapped types to filter object keys by value type:
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends (...args: any[]) => any ? K : never;
}[keyof T];
interface Service {
name: string;
load(): Promise<void>;
save(id: number): void;
}
type S = FunctionKeys<Service>;
// "load" | "save"
The mapped type produces an object where method keys map to themselves and others to never. Indexing back with [keyof T] unions the values, dropping never.
Common Pitfalls
Forgetting distribution. If you do not want union splitting, wrap the parameter in a tuple: [T] extends [U] ? ... : .... This prevents the union from being treated member by member.
Overusing any in your patterns. Writing (...args: any[]) => any is fine for extraction, but any elsewhere undermines the whole exercise. Prefer unknown when you only need a placeholder.
Building deeply nested conditionals that become impossible to debug. If your type has four nested ternaries, refactor into named helpers, just like you would extract functions.
Assuming the false branch always means “error.” Sometimes the false branch is a meaningful fallback, like preserving the original type. Pick branches deliberately and document with a comment.
Hitting recursion limits. TypeScript caps recursive instantiation depth. If you build something like a deep walker, use accumulator techniques and avoid creating unnecessary intermediate types.
Best Practices
Name your conditional types after the question they answer. IsArray<T> reads naturally; Cond1<T> does not. Names speed up reviews and reduce inline comments.
Use infer only inside extends clauses. Many people miss this. Outside, infer is a syntax error, and the type checker will be unhelpful.
Combine with Extract and Exclude from the standard lib. They are conditional types themselves and often save you from rolling your own. Extract<T, U> keeps members assignable to U; Exclude<T, U> removes them.
Write type tests. A small file with // @ts-expect-error comments and Expect<Equal<X, Y>> helpers will catch regressions when you tweak the conditional logic.
Reach for conditionals when overloads would explode. Two overloads are fine; ten are a sign you want one conditional type that captures the rule.
Wrap-up
Conditional types are TypeScript’s tool for expressing “it depends” at the type level. With extends, infer, and distribution, you can build utilities that adapt to whatever shape callers throw at them. Use them when patterns repeat, name them well, and write small type tests to lock behavior in. The result is a public API that feels almost magical because the right types just appear.
Related articles
- TypeScript TypeScript infer Keyword Explained
Master the infer keyword in TypeScript conditional types. Learn how to extract parts of complex types, build utility helpers, and write expressive generics.
- TypeScript TypeScript Generics with React
A practical guide to using TypeScript generics in React components, hooks, and props for safer, more reusable building blocks.
- TypeScript TypeScript Mapped Types Deep Dive
A practical tour of mapped types in TypeScript, including key remapping, modifiers, and patterns that build powerful generic utilities.
- TypeScript TypeScript Recursive Types Tutorial
Build recursive types in TypeScript: deep readonly, JSON, paths, and tuple manipulation. Learn how to write recursion that the compiler can actually evaluate.