Conditional Types in TypeScript
What are Conditional Types?
Conditional Types are a TypeScript construct that allows choosing a type based on a condition. They work like ternary operators, but for types.
Syntax
T extends U ? X : Y
If type T can be assigned to type U, the result is type X, otherwise type Y.
Simple Example
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<'hello'>; // true
How does it work?
- Checks if
Tis a subtype ofstring - If yes — returns
true - If no — returns
false
Practical Examples
Extracting Return Type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUserName() {
return 'John';
}
function getUserAge() {
return 25;
}
type NameType = ReturnType<typeof getUserName>; // string
type AgeType = ReturnType<typeof getUserAge>; // number
Filtering Types
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined>; // number
type C = NonNullable<boolean | null | undefined>; // boolean
Checking for Array
type IsArray<T> = T extends any[] ? true : false;
type A = IsArray<number[]>; // true
type B = IsArray<string>; // false
type C = IsArray<[1, 2, 3]>; // true (tuple is also array)
Distributive Conditional Types
When a conditional type is applied to a union type, it distributes over each member of the union.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string extends any ? string[] : never | number extends any ? number[] : never
// string[] | number[]
How does it work?
ToArray<string | number>
// Distributes as:
= ToArray<string> | ToArray<number>
= string[] | number[]
Disabling Distribution
Use square brackets to prevent distribution:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDist<string | number>; // (string | number)[]
The infer Keyword
infer allows "extracting" a type from a structure during conditional checking.
Unwrapping Promise Type
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<boolean>; // boolean
Extracting Function Argument Types
type FirstArg<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
function test(name: string, age: number) {
return { name, age };
}
type First = FirstArg<typeof test>; // string
Extracting Array Element Type
type ElementType<T> = T extends (infer E)[] ? E : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // boolean
Nested Conditional Types
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type A = TypeName<string>; // "string"
type B = TypeName<42>; // "number"
type C = TypeName<() => void>; // "function"
type D = TypeName<{}>; // "object"
Built-in Utilities Based on Conditional Types
TypeScript provides many built-in utilities implemented through conditional types.
Exclude
Excludes types from union:
type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
type B = Exclude<string | number, string>; // number
Extract
Extracts types from union:
type Extract<T, U> = T extends U ? T : never;
type A = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type B = Extract<string | number, number>; // number
NonNullable
Removes null and undefined:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number | undefined | null>; // number
ReturnType
Extracts function return type:
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
function getUserData() {
return { name: 'John', age: 25 };
}
type UserData = ReturnType<typeof getUserData>;
// { name: string; age: number; }
Advanced Patterns
Recursive Conditional Types
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface User {
name: string;
address: {
city: string;
country: string;
};
}
type ReadonlyUser = DeepReadonly<User>;
/*
{
readonly name: string;
readonly address: {
readonly city: string;
readonly country: string;
};
}
*/
Extracting Keys of Specific Type
type KeysOfType<T, U> = {
[K in keyof T]: T[K] extends U ? K : never;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type StringKeys = KeysOfType<User, string>; // "name" | "email"
type NumberKeys = KeysOfType<User, number>; // "id" | "age"
Making Required Fields Conditionally
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface User {
name?: string;
age?: number;
email?: string;
}
type UserWithName = RequireKeys<User, 'name'>;
// { name: string; age?: number; email?: string; }
Practical Use Cases
API Response Helper
type ApiResponse<T> = T extends { data: infer D }
? D
: T;
interface SuccessResponse {
data: { id: number; name: string };
status: 'success';
}
type Data = ApiResponse<SuccessResponse>;
// { id: number; name: string; }
Flatten Union Types
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>; // string
type B = Flatten<number[][]>; // number[]
type C = Flatten<(string | number)[]>; // string | number
Promise Chain Type
type UnwrapPromise<T> = T extends Promise<infer U>
? UnwrapPromise<U>
: T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<Promise<Promise<Promise<boolean>>>>; // boolean
Function or Value
type ValueOrFunction<T> = T | (() => T);
type Resolve<T> = T extends (...args: any[]) => infer R ? R : T;
type A = Resolve<string>; // string
type B = Resolve<() => number>; // number
Limitations and Features
Recursion Depth
TypeScript limits recursive type depth to prevent infinite loops:
// May lead to "Type instantiation is excessively deep" error
type DeepArray<T, N extends number = 10> =
N extends 0 ? T : DeepArray<T[], Decrement<N>>;
Order of Checks Matters
// Order is important
type TypeName<T> =
T extends any[] ? "array" : // Check array first
T extends object ? "object" : // Then object
T extends string ? "string" :
"other";
type A = TypeName<string[]>; // "array" (not "object")
Never in Conditional Types
type A = never extends string ? true : false; // true
// never is a subtype of any type
Comparison with Other Approaches
Before Conditional Types
// Had to use overloads
function wrap(x: string): string[];
function wrap(x: number): number[];
function wrap(x: any): any[] {
return [x];
}
With Conditional Types
type Wrap<T> = T extends any ? T[] : never;
function wrap<T>(x: T): Wrap<T> {
return [x] as Wrap<T>;
}
const a = wrap('hello'); // string[]
const b = wrap(42); // number[]
Common Mistakes
Forgetting about distributivity
type WrapInArray<T> = T extends any ? T[] : never;
type A = WrapInArray<string | number>; // string[] | number[]
// Expected (string | number)[], but got union of arrays
// Correct:
type WrapInArray<T> = [T] extends [any] ? T[] : never;
type B = WrapInArray<string | number>; // (string | number)[]
Incorrect use of infer
// Wrong
type Wrong<T> = T extends infer U ? U : never; // Meaningless
// Correct
type Correct<T> = T extends Promise<infer U> ? U : T;
Complex Logic in One Type
// Bad - hard to read
type Complex<T> = T extends string ? T extends `${infer F}${infer R}` ? F extends 'a' ? true : false : false : false;
// Good - split into parts
type StartsWithA<T> = T extends `a${string}` ? true : false;
type IsString<T> = T extends string ? true : false;
type Complex<T> = IsString<T> extends true ? StartsWithA<T> : false;
Conclusion
Conditional Types:
- Allow creating flexible and reusable types
- Foundation for many built-in TypeScript utilities
- Distribute over union types (distributive)
- Support type extraction via
infer - Can be recursive (with limitations)
- Critically important for advanced typing
In Interviews:
Important to be able to:
- Explain
T extends U ? X : Ysyntax - Show difference between distributive and non-distributive types
- Use
inferfor type extraction - Provide examples of built-in utilities
- Implement custom utilities based on conditional types