TypeScript Conditional Types
Create dynamic, flexible types that adapt based on type conditions
Introduction
Conditional types in TypeScript allow you to create non-uniform type mappings. They're like ternary expressions, but for types instead of values.
Basic Syntax
T extends U ? X : Y
Read as: "If type T is assignable to type U, then the result is type X; otherwise, it's type Y."
Basic Examples
Simple Conditionals
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<123>; // false
type C = IsString<string>; // true
Nullable Handling
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number>; // number
Practical Use Cases
Extracting Return Types
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function getUser() {
return { id: 1, name: 'John' };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; }
Extracting Array Element Types
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string
Flattening Nested Arrays
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<number[][]>; // number[]
type B = Flatten<string[]>; // string
type C = Flatten<number>; // number
Distributive Conditional Types
Conditional types automatically distribute over union types.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// string[] | number[] (distributes)
// Not: (string | number)[]
Preventing Distribution
Wrap types in tuples to prevent distribution:
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type A = ToArrayNonDistributive<string | number>;
// (string | number)[] (doesn't distribute)
Advanced Patterns
Type Extraction with infer
The infer keyword extracts types from complex structures.
// Extract Promise resolution type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
// Extract function parameters
type FirstParameter<T> = T extends (arg1: infer P, ...args: any[]) => any
? P
: never;
type A = FirstParameter<(name: string, age: number) => void>; // string
Recursive Conditional Types
// Remove all nulls from nested object
type RemoveNull<T> = {
[P in keyof T]: T[P] extends null
? never
: T[P] extends object
? RemoveNull<T[P]>
: T[P];
};
interface User {
id: number;
name: string | null;
address: {
street: string;
city: string | null;
};
}
type CleanUser = RemoveNull<User>;
// { id: number; name: string; address: { street: string; city: string; } }
Chaining Conditionals
type Simplify<T> =
T extends Array<infer U>
? Array<Simplify<U>>
: T extends object
? { [K in keyof T]: Simplify<T[K]> }
: T;
Built-in Conditional Types
TypeScript includes several conditional types in its standard library:
Exclude<T, U>- Exclude types from a unionExtract<T, U>- Extract types from a unionNonNullable<T>- Remove null and undefinedReturnType<T>- Get function return typeInstanceType<T>- Get instance type from constructorParameters<T>- Get function parameter types
Common Pitfalls
Over-Engineering
// ❌ Too complex
type ComplexType<T> = T extends infer U
? U extends (...args: any[]) => any
? ReturnType<U>
: U
: never;
// ✅ Simpler alternative
type SimpleType<T> = T extends Function ? ReturnType<T> : T;
Losing Type Information
// ❌ Loses specificity
type Lossy<T> = T extends object ? any : T;
// ✅ Preserves information
type Safe<T> = T extends object ? Record<string, unknown> : T;
Infinite Recursion
// ❌ Infinite loop
type Infinite<T> = T extends object ? Infinite<T> : T;
// ✅ Add termination
type Safe<T, Depth extends number = 5> = Depth extends 0
? T
: T extends object
? Safe<T, Prev[Depth]>
: T;
Best Practices
Start Simple
Use conditional types only when simpler alternatives don't work.
Document Complex Types
Add comments explaining what complex conditional types do.
Test Your Types
Use type tests to verify conditional types work as expected:
type AssertEqual<T, Expected> = [T] extends [Expected]
? [Expected] extends [T]
? true
: false
: false;
type Test = AssertEqual<ReturnType<() => string>, string>; // true
Use infer Sparingly
Only use infer when you need to extract types from complex structures.
Conclusion
Conditional types unlock powerful type-level programming in TypeScript. Use them wisely to create flexible, reusable type utilities while maintaining readability.
#typescript #types #advanced-types