Back to Blog
TypeScriptTypesAdvancedGenerics

TypeScript Conditional Types Guide

Master TypeScript conditional types. From basic patterns to advanced type inference and manipulation.

B
Bootspring Team
Engineering
July 12, 2020
8 min read

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

Type 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>; // false

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

Share this article

Help spread the word about Bootspring