Back to Blog
TypeScriptConditional TypesAdvanced TypesType System

TypeScript Conditional Types Deep Dive

Master conditional types in TypeScript. From basic syntax to distributive types to advanced patterns.

B
Bootspring Team
Engineering
September 29, 2021
7 min read

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>; // never

Inferring 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>; // string

Distributive 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 | boolean

Recursive 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 optional

Mapped 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.

Share this article

Help spread the word about Bootspring