Conditional types enable complex type logic based on conditions. Here's how to use them effectively.
Basic Conditional Types#
1// Syntax: T extends U ? X : Y
2type IsString<T> = T extends string ? true : false;
3
4type A = IsString<string>; // true
5type B = IsString<number>; // false
6
7// With generics
8type TypeName<T> = T extends string
9 ? 'string'
10 : T extends number
11 ? 'number'
12 : T extends boolean
13 ? 'boolean'
14 : T extends undefined
15 ? 'undefined'
16 : T extends Function
17 ? 'function'
18 : 'object';
19
20type T1 = TypeName<string>; // 'string'
21type T2 = TypeName<() => void>; // 'function'
22type T3 = TypeName<string[]>; // 'object'Distributive Conditional Types#
1// Conditional types distribute over unions
2type ToArray<T> = T extends any ? T[] : never;
3
4type StringOrNumberArray = ToArray<string | number>;
5// string[] | number[]
6
7// Preventing distribution
8type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
9
10type StringOrNumberTuple = ToArrayNonDist<string | number>;
11// (string | number)[]
12
13// Practical example
14type NonNullable<T> = T extends null | undefined ? never : T;
15
16type A = NonNullable<string | null | undefined>;
17// string
18
19// Exclude and Extract
20type Exclude<T, U> = T extends U ? never : T;
21type Extract<T, U> = T extends U ? T : never;
22
23type Numbers = Extract<string | number | boolean, number>;
24// number
25
26type NotNumbers = Exclude<string | number | boolean, number>;
27// string | booleanType Inference with infer#
1// Extract return type
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4type FuncReturn = ReturnType<() => string>; // string
5
6// Extract parameter types
7type Parameters<T> = T extends (...args: infer P) => any ? P : never;
8
9type FuncParams = Parameters<(a: string, b: number) => void>;
10// [string, number]
11
12// Extract array element type
13type ArrayElement<T> = T extends (infer E)[] ? E : never;
14
15type Element = ArrayElement<string[]>; // string
16
17// Extract promise value
18type Awaited<T> = T extends Promise<infer U>
19 ? Awaited<U> // Recursively unwrap
20 : T;
21
22type Value = Awaited<Promise<Promise<string>>>; // string
23
24// Extract first and rest
25type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
26type Rest<T extends any[]> = T extends [any, ...infer R] ? R : never;
27
28type F = First<[1, 2, 3]>; // 1
29type R = Rest<[1, 2, 3]>; // [2, 3]
30
31// Extract function this type
32type ThisParameter<T> = T extends (this: infer U, ...args: any[]) => any
33 ? U
34 : unknown;Practical Patterns#
1// Props of component
2type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
3
4const Button: React.FC<{ label: string; onClick: () => void }> = () => null;
5type ButtonProps = ComponentProps<typeof Button>;
6// { label: string; onClick: () => void }
7
8// Filter object keys by value type
9type KeysOfType<T, V> = {
10 [K in keyof T]: T[K] extends V ? K : never;
11}[keyof T];
12
13interface User {
14 id: number;
15 name: string;
16 email: string;
17 age: number;
18}
19
20type StringKeys = KeysOfType<User, string>;
21// 'name' | 'email'
22
23type NumberKeys = KeysOfType<User, number>;
24// 'id' | 'age'
25
26// Pick by value type
27type PickByType<T, V> = Pick<T, KeysOfType<T, V>>;
28
29type StringFields = PickByType<User, string>;
30// { name: string; email: string }
31
32// Omit by value type
33type OmitByType<T, V> = Omit<T, KeysOfType<T, V>>;
34
35type NonStringFields = OmitByType<User, string>;
36// { id: number; age: number }Function Overloads#
1// Conditional return types
2function process<T extends string | number>(
3 value: T
4): T extends string ? string[] : number {
5 if (typeof value === 'string') {
6 return value.split('') as any;
7 }
8 return (value * 2) as any;
9}
10
11const strResult = process('hello'); // string[]
12const numResult = process(42); // number
13
14// Multiple conditions
15type ResponseType<T> = T extends 'json'
16 ? object
17 : T extends 'text'
18 ? string
19 : T extends 'blob'
20 ? Blob
21 : never;
22
23async function fetchData<T extends 'json' | 'text' | 'blob'>(
24 url: string,
25 type: T
26): Promise<ResponseType<T>> {
27 const response = await fetch(url);
28
29 switch (type) {
30 case 'json':
31 return response.json() as Promise<ResponseType<T>>;
32 case 'text':
33 return response.text() as Promise<ResponseType<T>>;
34 case 'blob':
35 return response.blob() as Promise<ResponseType<T>>;
36 default:
37 throw new Error('Invalid type');
38 }
39}Recursive Types#
1// Deep partial
2type DeepPartial<T> = T extends object
3 ? { [P in keyof T]?: DeepPartial<T[P]> }
4 : T;
5
6interface NestedConfig {
7 server: {
8 port: number;
9 host: string;
10 ssl: {
11 enabled: boolean;
12 cert: string;
13 };
14 };
15}
16
17type PartialConfig = DeepPartial<NestedConfig>;
18
19// Deep readonly
20type DeepReadonly<T> = T extends (infer U)[]
21 ? DeepReadonlyArray<U>
22 : T extends object
23 ? DeepReadonlyObject<T>
24 : T;
25
26interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}
27
28type DeepReadonlyObject<T> = {
29 readonly [P in keyof T]: DeepReadonly<T[P]>;
30};
31
32// Flatten array type
33type Flatten<T> = T extends (infer U)[]
34 ? Flatten<U>
35 : T;
36
37type Flat = Flatten<number[][][][]>; // number
38
39// JSON types
40type JSONValue =
41 | string
42 | number
43 | boolean
44 | null
45 | JSONValue[]
46 | { [key: string]: JSONValue };
47
48type JSONify<T> = T extends string | number | boolean | null
49 ? T
50 : T extends Function
51 ? never
52 : T extends (infer U)[]
53 ? JSONify<U>[]
54 : T extends object
55 ? { [K in keyof T]: JSONify<T[K]> }
56 : never;Advanced Inference#
1// Infer multiple parts
2type ParseQueryString<S extends string> =
3 S extends `${infer K}=${infer V}&${infer Rest}`
4 ? { [P in K]: V } & ParseQueryString<Rest>
5 : S extends `${infer K}=${infer V}`
6 ? { [P in K]: V }
7 : {};
8
9type Query = ParseQueryString<'name=alice&age=25&city=nyc'>;
10// { name: 'alice' } & { age: '25' } & { city: 'nyc' }
11
12// Event handler inference
13type EventHandler<T extends string> = T extends `on${infer Event}`
14 ? Event extends Capitalize<Event>
15 ? Event
16 : never
17 : never;
18
19type ClickEvent = EventHandler<'onClick'>; // 'Click'
20type InvalidEvent = EventHandler<'click'>; // never
21
22// Path parameters
23type ExtractRouteParams<T extends string> =
24 T extends `${infer _Start}:${infer Param}/${infer Rest}`
25 ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
26 : T extends `${infer _Start}:${infer Param}`
27 ? { [K in Param]: string }
28 : {};
29
30type Params = ExtractRouteParams<'/users/:userId/posts/:postId'>;
31// { userId: string; postId: string }Utility Type Implementations#
1// Built-in utilities reimplemented
2type MyExclude<T, U> = T extends U ? never : T;
3type MyExtract<T, U> = T extends U ? T : never;
4type MyNonNullable<T> = T extends null | undefined ? never : T;
5type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
6
7// InstanceType
8type MyInstanceType<T extends new (...args: any[]) => any> =
9 T extends new (...args: any[]) => infer R ? R : never;
10
11class MyClass {
12 name: string;
13}
14type Instance = MyInstanceType<typeof MyClass>; // MyClass
15
16// ConstructorParameters
17type MyConstructorParameters<T extends new (...args: any[]) => any> =
18 T extends new (...args: infer P) => any ? P : never;
19
20// OmitThisParameter
21type MyOmitThisParameter<T> = T extends (this: any, ...args: infer A) => infer R
22 ? (...args: A) => R
23 : T;
24
25// ThisParameterType
26type MyThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any
27 ? U
28 : unknown;Type Guards#
1// Type predicate with conditional
2type AssertType<T, U extends T> = T extends U ? U : never;
3
4// Narrow array type
5type NarrowArray<T, U> = T extends any[]
6 ? Extract<T[number], U>[]
7 : never;
8
9// Filter nullable from array
10type FilterNull<T> = T extends (infer U | null | undefined)[]
11 ? NonNullable<U>[]
12 : never;
13
14type Cleaned = FilterNull<(string | null | undefined)[]>;
15// string[]
16
17// Strict property check
18type StrictPropertyCheck<T, U, V> = U extends keyof T
19 ? T[U] extends V
20 ? true
21 : false
22 : false;
23
24interface User {
25 name: string;
26 age: number;
27}
28
29type HasStringName = StrictPropertyCheck<User, 'name', string>; // true
30type HasNumberName = StrictPropertyCheck<User, 'name', number>; // falsePractical Examples#
1// API response handler
2type ApiResponse<T> = T extends 'user'
3 ? { id: number; name: string }
4 : T extends 'post'
5 ? { id: number; title: string; body: string }
6 : T extends 'comment'
7 ? { id: number; postId: number; text: string }
8 : never;
9
10async function fetchApi<T extends 'user' | 'post' | 'comment'>(
11 endpoint: T
12): Promise<ApiResponse<T>> {
13 const response = await fetch(`/api/${endpoint}`);
14 return response.json();
15}
16
17// Form field types
18type FieldType<T> = T extends string
19 ? 'text'
20 : T extends number
21 ? 'number'
22 : T extends boolean
23 ? 'checkbox'
24 : T extends Date
25 ? 'date'
26 : 'text';
27
28type FormFields<T> = {
29 [K in keyof T]: {
30 name: K;
31 type: FieldType<T[K]>;
32 value: T[K];
33 };
34};
35
36interface UserForm {
37 name: string;
38 age: number;
39 active: boolean;
40 birthDate: Date;
41}
42
43type UserFormFields = FormFields<UserForm>;Best Practices#
Design:
✓ Use infer for extracting types
✓ Leverage distribution over unions
✓ Wrap in tuple to prevent distribution
✓ Use recursion carefully
Readability:
✓ Name complex conditions clearly
✓ Break down nested conditions
✓ Use intermediate types
✓ Document non-obvious logic
Performance:
✓ Avoid deep recursion
✓ Cache computed types
✓ Limit union size
✓ Use constraints
Avoid:
✗ Over-complicated conditions
✗ Deeply nested ternaries
✗ Unclear type logic
✗ Excessive distribution
Conclusion#
Conditional types enable powerful type-level programming in TypeScript. Use them for type transformations, inference, and complex type logic. Combine with infer for extracting types and recursion for deep operations. Keep types readable by breaking down complex conditions.