Conditional types enable type-level programming. Here's how to use them effectively.
Basic Syntax#
1// 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// Practical example
8type MessageOf<T> = T extends { message: string } ? T['message'] : never;
9
10interface Email {
11 message: string;
12 subject: string;
13}
14
15type EmailMessage = MessageOf<Email>; // string
16type NumberMessage = MessageOf<number>; // neverInferring Types#
1// Use 'infer' to extract types
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4function getUser() {
5 return { id: 1, name: 'John' };
6}
7
8type User = ReturnType<typeof getUser>; // { id: number; name: string }
9
10// Extract array element type
11type ArrayElement<T> = T extends (infer E)[] ? E : never;
12
13type StringArrayElement = ArrayElement<string[]>; // string
14
15// Extract promise value
16type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
17
18type PromiseValue = UnwrapPromise<Promise<number>>; // number
19type DirectValue = UnwrapPromise<string>; // string
20
21// Infer function parameters
22type Parameters<T> = T extends (...args: infer P) => any ? P : never;
23
24function greet(name: string, age: number): string {
25 return `Hello ${name}, you are ${age}`;
26}
27
28type GreetParams = Parameters<typeof greet>; // [string, number]
29
30// Infer first parameter
31type FirstParam<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;
32
33type FirstGreetParam = FirstParam<typeof greet>; // stringDistributive Conditional Types#
1// Conditional types distribute over unions
2type ToArray<T> = T extends any ? T[] : never;
3
4type StringOrNumber = string | number;
5type Arrays = ToArray<StringOrNumber>; // string[] | number[]
6
7// Prevent distribution with tuple
8type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
9
10type SingleArray = ToArrayNonDistributive<string | number>; // (string | number)[]
11
12// Filter union types
13type ExtractString<T> = T extends string ? T : never;
14
15type Mixed = string | number | boolean | 'hello' | 42;
16type Strings = ExtractString<Mixed>; // string | 'hello'
17
18// Built-in utility types use distribution
19type Extract<T, U> = T extends U ? T : never;
20type Exclude<T, U> = T extends U ? never : T;
21
22type OnlyStrings = Extract<string | number | boolean, string>; // string
23type NoStrings = Exclude<string | number | boolean, string>; // number | booleanRecursive Conditional Types#
1// Deep unwrap promises
2type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
3
4type NestedPromise = Promise<Promise<Promise<string>>>;
5type Unwrapped = DeepUnwrap<NestedPromise>; // string
6
7// Flatten nested arrays
8type Flatten<T> = T extends (infer U)[]
9 ? Flatten<U>
10 : T;
11
12type NestedArray = number[][][];
13type FlatNumber = Flatten<NestedArray>; // number
14
15// Deep readonly
16type DeepReadonly<T> = T extends object
17 ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
18 : T;
19
20interface User {
21 name: string;
22 profile: {
23 email: string;
24 settings: {
25 theme: string;
26 };
27 };
28}
29
30type ReadonlyUser = DeepReadonly<User>;
31// All nested properties are readonly
32
33// Deep partial
34type DeepPartial<T> = T extends object
35 ? { [K in keyof T]?: DeepPartial<T[K]> }
36 : T;
37
38type PartialUser = DeepPartial<User>;
39// All nested properties are optionalMapped Types with Conditionals#
1// Transform property types conditionally
2type FunctionProperties<T> = {
3 [K in keyof T]: T[K] extends Function ? K : never;
4}[keyof T];
5
6interface Example {
7 name: string;
8 age: number;
9 greet(): void;
10 calculate(x: number): number;
11}
12
13type FuncProps = FunctionProperties<Example>; // 'greet' | 'calculate'
14
15// Extract non-function properties
16type NonFunctionProperties<T> = {
17 [K in keyof T]: T[K] extends Function ? never : K;
18}[keyof T];
19
20type DataProps = NonFunctionProperties<Example>; // 'name' | 'age'
21
22// Pick only certain property types
23type PickByType<T, U> = {
24 [K in keyof T as T[K] extends U ? K : never]: T[K];
25};
26
27type StringProps = PickByType<Example, string>; // { name: string }
28
29// Omit by type
30type OmitByType<T, U> = {
31 [K in keyof T as T[K] extends U ? never : K]: T[K];
32};
33
34type NonStringProps = OmitByType<Example, string>; // { age: number; greet(): void; ... }Template Literal Conditionals#
1// Extract parts from string types
2type ExtractRouteParams<T extends string> =
3 T extends `${string}:${infer Param}/${infer Rest}`
4 ? Param | ExtractRouteParams<`/${Rest}`>
5 : T extends `${string}:${infer Param}`
6 ? Param
7 : never;
8
9type RouteParams = ExtractRouteParams<'/users/:userId/posts/:postId'>;
10// 'userId' | 'postId'
11
12// Build object from route params
13type RouteParamObject<T extends string> = {
14 [K in ExtractRouteParams<T>]: string;
15};
16
17type UserPostParams = RouteParamObject<'/users/:userId/posts/:postId'>;
18// { userId: string; postId: string }
19
20// Transform string types
21type CamelToSnake<S extends string> =
22 S extends `${infer T}${infer U}`
23 ? T extends Uppercase<T>
24 ? `_${Lowercase<T>}${CamelToSnake<U>}`
25 : `${T}${CamelToSnake<U>}`
26 : S;
27
28type Snake = CamelToSnake<'userName'>; // 'user_name'
29
30// Get event handler name
31type EventHandler<T extends string> = T extends `on${infer Event}`
32 ? Lowercase<Event>
33 : never;
34
35type Handler = EventHandler<'onClick'>; // 'click'Practical Patterns#
1// API response wrapper
2type ApiResponse<T> = T extends void
3 ? { success: true }
4 : { success: true; data: T };
5
6type VoidResponse = ApiResponse<void>; // { success: true }
7type UserResponse = ApiResponse<User>; // { success: true; data: User }
8
9// Nullable to optional
10type NullableToOptional<T> = {
11 [K in keyof T as null extends T[K] ? never : K]: T[K];
12} & {
13 [K in keyof T as null extends T[K] ? K : never]?: Exclude<T[K], null>;
14};
15
16interface Input {
17 name: string;
18 email: string | null;
19 age: number | null;
20}
21
22type Output = NullableToOptional<Input>;
23// { name: string; email?: string; age?: number }
24
25// Promisify function type
26type Promisify<T> = T extends (...args: infer A) => infer R
27 ? (...args: A) => Promise<R>
28 : never;
29
30function sync(x: number): string {
31 return x.toString();
32}
33
34type AsyncSync = Promisify<typeof sync>; // (x: number) => Promise<string>
35
36// Extract component props
37type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
38
39const MyComponent: React.FC<{ name: string; age: number }> = () => null;
40type Props = ComponentProps<typeof MyComponent>; // { name: string; age: number }Type Constraints#
1// Ensure type extends constraint
2type EnsureArray<T> = T extends any[] ? T : never;
3
4type ValidArray = EnsureArray<string[]>; // string[]
5type Invalid = EnsureArray<string>; // never
6
7// Assert types
8type AssertExtends<T, U> = T extends U ? T : never;
9
10type AssertString = AssertExtends<'hello', string>; // 'hello'
11type AssertFail = AssertExtends<123, string>; // never
12
13// Require properties
14type RequireKeys<T, K extends keyof T> = T & {
15 [P in K]-?: T[P];
16};
17
18interface Config {
19 host?: string;
20 port?: number;
21 ssl?: boolean;
22}
23
24type RequiredConfig = RequireKeys<Config, 'host' | 'port'>;
25// { host: string; port: number; ssl?: boolean }
26
27// Make specific properties optional
28type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
29
30interface User {
31 id: number;
32 name: string;
33 email: string;
34}
35
36type CreateUser = OptionalKeys<User, 'id'>;
37// { name: string; email: string; id?: number }Best Practices#
Design:
✓ Start simple, add complexity as needed
✓ Use meaningful type names
✓ Document complex conditional types
✓ Test with different type inputs
Performance:
✓ Avoid deeply nested conditionals
✓ Use type caching when possible
✓ Limit recursion depth
✓ Profile compile times
Readability:
✓ Break complex types into smaller pieces
✓ Use helper types
✓ Add comments for tricky logic
✓ Provide examples in JSDoc
Conclusion#
Conditional types enable powerful type transformations in TypeScript. Use infer to extract types, leverage distribution for union manipulation, and combine with mapped types for flexible type utilities. Keep types readable and well-documented as complexity grows.