TypeScript Conditional Types
Conditional types allow TypeScript to choose one type or another based on a condition. They follow an if-else pattern at the type level: "if this type extends that type, use type A; otherwise use type B." Conditional types make it possible to write generic utilities that adapt their output type based on their input type.
Basic Syntax
T extends U ? TypeIfTrue : TypeIfFalse | | | | | +-- Type returned when condition is FALSE | +----------------- Type returned when condition is TRUE +-------------------------------- Type being tested
This mirrors the JavaScript ternary operator (condition ? a : b), but operates on types instead of values.
Simple Conditional Type
type IsString<T> = T extends string ? "yes" : "no"; type Check1 = IsString<string>; // "yes" type Check2 = IsString<number>; // "no" type Check3 = IsString<"hello">; // "yes" — literal string extends string type Check4 = IsString<boolean>; // "no"
Conditional Types with Generics
// Returns string when T is number, returns number when T is string type Flip<T> = T extends number ? string : T extends string ? number : never; type A = Flip<number>; // string type B = Flip<string>; // number type C = Flip<boolean>; // never
Distributive Conditional Types
When a conditional type receives a union type, it distributes the condition over each member of the union individually. TypeScript applies the condition to each type in the union and combines the results.
type ToArray<T> = T extends any ? T[] : never; type Result1 = ToArray<string>; // string[] type Result2 = ToArray<number>; // number[] type Result3 = ToArray<string | number>; // string[] | number[] // ^^^^^^^^^^^^^^ // Distributed: string[] | number[] // NOT: (string | number)[]
Distribution over union:
T = string | number
ToArray<string | number>
↓ distributes
ToArray<string> | ToArray<number>
↓
string[] | number[]
Preventing Distribution
Wrapping both sides in square brackets prevents distribution and treats the union as a single type.
type NoDistribute<T> = [T] extends [any] ? T[] : never; type R1 = NoDistribute<string | number>; // (string | number)[]
infer Keyword
The infer keyword extracts a type from within another type during the conditional check. It captures part of the matched type and gives it a name for use in the true branch.
T extends (infer U)[] ? U : never
|
+-- Capture the element type of T as U
If T is string[] → U is string
If T is number[] → U is number
// Extract element type from an array type type ElementType<T> = T extends (infer U)[] ? U : never; type E1 = ElementType<string[]>; // string type E2 = ElementType<number[]>; // number type E3 = ElementType<boolean[][]>; // boolean[] type E4 = ElementType<string>; // never — not an array
// Extract the return type of a function type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type F1 = ReturnType<() => string>; // string type F2 = ReturnType<(x: number) => boolean>; // boolean type F3 = ReturnType<() => number[]>; // number[]
Extracting Promise Resolution Type
type Awaited<T> = T extends Promise<infer U> ? U : T; type R1 = Awaited<Promise<string>>; // string type R2 = Awaited<Promise<number>>; // number type R3 = Awaited<boolean>; // boolean (not a Promise — returns as-is)
Excluding Types from a Union
// Built into TypeScript as Exclude<T, U> type MyExclude<T, U> = T extends U ? never : T; type T1 = MyExclude<string | number | boolean, string>; // Result: number | boolean (string is excluded) type T2 = MyExclude<"a" | "b" | "c" | "d", "a" | "c">; // Result: "b" | "d"
Filtering Object Properties by Type
// Keep only properties whose values are of a specified type
type PickByValue<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K]
};
type Product = {
id: number;
name: string;
price: number;
available: boolean;
category: string;
};
type NumberProps = PickByValue<Product, number>;
// { id: number; price: number }
type StringProps = PickByValue<Product, string>;
// { name: string; category: string }
Nested Conditional Types
// Classify a type
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends null ? "null" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type N1 = TypeName<string>; // "string"
type N2 = TypeName<number>; // "number"
type N3 = TypeName<() => void>; // "function"
type N4 = TypeName<{ a: 1 }>; // "object"
Real-World Utility Pattern
// Make all function properties of an object return Promises
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K];
};
type UserService = {
getUser(id: number): string;
getCount(): number;
appName: string;
};
type AsyncUserService = Promisify<UserService>;
// {
// getUser(id: number): Promise<string>;
// getCount(): Promise<number>;
// appName: string; ← not a function, unchanged
// }
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Basic conditional | T extends U ? A : B | Choose type based on condition |
| Distribution | Auto on union types | Apply condition to each union member |
| Prevent distribution | [T] extends [U] | Treat union as whole |
| infer | T extends F<infer U> | Extract a type from within another |
| Nested ternary | T extends A ? X : T extends B ? Y : Z | Multiple type branches |
