Conditional Types & Type Manipulation
Conditional types are TypeScript’s most powerful feature for type-level programming. They enable you to create types that adapt based on other types, leading to incredibly flexible and intelligent type systems.
Understanding Conditional Types
Basic Syntax
Conditional types follow the pattern T extends U ? X : Y:
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<"hello">; // truetype Test2 = IsString<number>; // falsetype Test3 = IsString<string>; // trueReal-World Example: API Response Types
type ApiResponse<T> = T extends string ? { message: T; type: "text" } : T extends number ? { count: T; type: "numeric" } : T extends boolean ? { success: T; type: "boolean" } : { data: T; type: "object" };
function handleApiResponse<T>(response: ApiResponse<T>) { // TypeScript knows the exact shape based on T if (response.type === "text") { console.log(response.message); // TypeScript knows this exists } else if (response.type === "numeric") { console.log(response.count); // TypeScript knows this exists }}Distributive Conditional Types
When conditional types are applied to union types, they distribute over each member:
type ToArray<T> = T extends any ? T[] : never;
// Distributes over union memberstype Result = ToArray<string | number>; // string[] | number[]
// To prevent distribution, wrap in a tupletype ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;type Result2 = ToArrayNonDistributive<string | number>; // (string | number)[]Practical Example: Event Handler Types
type EventMap = { click: MouseEvent; keydown: KeyboardEvent; scroll: Event;};
type EventHandler<T extends keyof EventMap> = T extends any ? (event: EventMap[T]) => void : never;
type ClickHandler = EventHandler<"click">; // (event: MouseEvent) => voidtype KeyHandler = EventHandler<"keydown">; // (event: KeyboardEvent) => void
// For multiple eventstype MultiEventHandler = EventHandler<"click" | "keydown">;// Result: ((event: MouseEvent) => void) | ((event: KeyboardEvent) => void)The infer Keyword
infer allows you to extract and capture types within conditional types:
Basic Type Extraction
// Extract return typetype ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type FuncReturn = ReturnType<() => string>; // stringtype AsyncReturn = ReturnType<() => Promise<number>>; // Promise<number>
// Extract parameter typestype Parameters<T> = T extends (...args: infer P) => any ? P : never;
type FuncParams = Parameters<(a: string, b: number) => void>; // [string, number]Advanced Inference Patterns
// Extract array element typetype ArrayElement<T> = T extends (infer U)[] ? U : never;type StringArrayElement = ArrayElement<string[]>; // string
// Extract promise valuetype PromiseValue<T> = T extends Promise<infer U> ? U : T;type AsyncValue = PromiseValue<Promise<User>>; // Usertype SyncValue = PromiseValue<string>; // string
// Extract object value typestype ValueOf<T> = T extends { [key: string]: infer V } ? V : never;type UserValues = ValueOf<{ name: string; age: number }>; // string | number
// Extract function from event handlertype HandlerFunction<T> = T extends { handler: infer F } ? F : never;type Handler = HandlerFunction<{ handler: (x: number) => string }>; // (x: number) => stringComplex Inference: Parsing Function Signatures
// Extract first parametertype Head<T> = T extends [infer H, ...any[]] ? H : never;
// Extract all but first parametertype Tail<T> = T extends [any, ...infer T] ? T : never;
// Extract last parametertype Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type FirstParam = Head<[string, number, boolean]>; // stringtype RestParams = Tail<[string, number, boolean]>; // [number, boolean]type LastParam = Last<[string, number, boolean]>; // boolean
// Curry function typetype Curry<T> = T extends (arg: infer A, ...rest: infer R) => infer Return ? R extends [] ? (arg: A) => Return : (arg: A) => Curry<(...args: R) => Return> : never;
function add(a: number, b: number, c: number): number { return a + b + c;}
type CurriedAdd = Curry<typeof add>;// (arg: number) => (arg: number) => (arg: number) => numberTemplate Literal Types
Template literal types enable sophisticated string manipulation at the type level:
Basic Template Literals
type World = "world";type Greeting = `hello ${World}`; // "hello world"
type EmailLocaleIDs = "welcome_email" | "email_heading";type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;// "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"Advanced String Manipulation
// Capitalize first lettertype Capitalize<S extends string> = S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : S;
type CapitalizedHello = Capitalize<"hello">; // "Hello"
// Convert to camelCasetype CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}` ? `${P1}${Uppercase<P2>}${CamelCase<P3>}` : S;
type CamelCased = CamelCase<"hello_world_example">; // "helloWorldExample"
// Extract file extensiontype GetExtension<T extends string> = T extends `${string}.${infer Ext}` ? Ext : never;
type JSExtension = GetExtension<"app.js">; // "js"type TSExtension = GetExtension<"component.tsx">; // "tsx"Practical Example: API Route Types
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";type Route = "/users" | "/posts" | "/comments";
type ApiEndpoint<M extends HttpMethod, R extends Route> = `${M} ${R}`;
type UserEndpoints = ApiEndpoint<"GET" | "POST", "/users">;// "GET /users" | "POST /users"
// Route parameter extractiontype ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? { [K in Param]: string } & ExtractRouteParams<`/${Rest}`> : T extends `${string}:${infer Param}` ? { [K in Param]: string } : {};
type UserRouteParams = ExtractRouteParams<"/users/:id/posts/:postId">;// { id: string; postId: string }Advanced Type Manipulation Patterns
Recursive Type Definitions
// JSON type definitiontype JSONValue = | string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
// Deep partialtype DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];};
// Deep requiredtype DeepRequired<T> = { [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];};
// Flatten nested objectstype Flatten<T> = T extends object ? T extends any[] ? T : { [K in keyof T]: Flatten<T[K]> } : T;Path-Based Type Access
// Get nested property type by pathtype GetByPath<T, P extends string> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? GetByPath<T[Key], Rest> : never : P extends keyof T ? T[P] : never;
interface User { profile: { personal: { name: string; age: number; }; settings: { theme: "light" | "dark"; notifications: boolean; }; };}
type UserName = GetByPath<User, "profile.personal.name">; // stringtype UserTheme = GetByPath<User, "profile.settings.theme">; // "light" | "dark"
// Set nested property type by pathtype SetByPath<T, P extends string, V> = P extends `${infer Key}.${infer Rest}` ? Key extends keyof T ? { [K in keyof T]: K extends Key ? SetByPath<T[K], Rest, V> : T[K] } : never : P extends keyof T ? { [K in keyof T]: K extends P ? V : T[K] } : never;
type UpdatedUser = SetByPath<User, "profile.personal.name", number>;// User with name changed from string to numberConditional Type Utilities
Building Advanced Utilities
// NonNullable that works with nested propertiestype DeepNonNullable<T> = { [P in keyof T]-?: T[P] extends object ? DeepNonNullable<T[P]> : NonNullable<T[P]>;};
// Extract function types from an objecttype FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never;}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
interface Example { name: string; age: number; greet(): void; calculate(x: number): number;}
type ExampleFunctions = FunctionProperties<Example>;// { greet(): void; calculate(x: number): number; }
// Mutable version of readonly typestype Mutable<T> = { -readonly [P in keyof T]: T[P];};
type ReadonlyUser = { readonly id: string; readonly name: string;};
type MutableUser = Mutable<ReadonlyUser>;// { id: string; name: string; }Conditional Type Chains
// Complex conditional chaintype ProcessType<T> = T extends string ? T extends `${infer Prefix}_${infer Suffix}` ? { prefix: Prefix; suffix: Suffix } : { value: T } : T extends number ? T extends 0 ? { zero: true } : { positive: T extends infer N ? N : never } : T extends boolean ? { flag: T } : { unknown: T };
type ProcessedString = ProcessType<"hello_world">; // { prefix: "hello"; suffix: "world" }type ProcessedNumber = ProcessType<42>; // { positive: 42 }type ProcessedBoolean = ProcessType<true>; // { flag: true }Performance Optimization
Avoiding Infinite Recursion
// Depth-limited recursiontype DeepReadonly<T, Depth extends number = 5> = Depth extends 0 ? T : { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P], Prev<Depth>> : T[P]; };
type Prev<T extends number> = T extends 5 ? 4 : T extends 4 ? 3 : T extends 3 ? 2 : T extends 2 ? 1 : T extends 1 ? 0 : never;Lazy Evaluation
// Lazy conditional typestype LazyConditional<T> = T extends any ? T extends string ? StringProcessor<T> : T extends number ? NumberProcessor<T> : DefaultProcessor<T> : never;
type StringProcessor<T extends string> = { type: "string"; value: T; length: T["length"];};
type NumberProcessor<T extends number> = { type: "number"; value: T; isPositive: T extends 0 ? false : true;};
type DefaultProcessor<T> = { type: "unknown"; value: T;};Real-World Applications
Form Validation Types
type ValidationRule<T> = { required?: boolean; min?: T extends string ? number : T extends number ? T : never; max?: T extends string ? number : T extends number ? T : never; pattern?: T extends string ? RegExp : never;};
type FormSchema<T> = { [K in keyof T]: ValidationRule<T[K]>;};
interface LoginForm { username: string; password: string; age: number;}
type LoginValidation = FormSchema<LoginForm>;// {// username: ValidationRule<string>;// password: ValidationRule<string>;// age: ValidationRule<number>;// }
const loginSchema: LoginValidation = { username: { required: true, min: 3, max: 20 }, password: { required: true, min: 8, pattern: /^(?=.*[A-Za-z])(?=.*\d)/ }, age: { required: true, min: 18, max: 120 }};State Machine Types
type StateMachine<States extends string, Events extends string> = { [S in States]: { [E in Events]?: States; };};
type TrafficLightStates = "red" | "yellow" | "green";type TrafficLightEvents = "timer" | "emergency";
type TrafficLightMachine = StateMachine<TrafficLightStates, TrafficLightEvents>;
const trafficLight: TrafficLightMachine = { red: { timer: "green", emergency: "yellow" }, yellow: { timer: "red" }, green: { timer: "yellow", emergency: "red" }};
// Type-safe state transitionstype GetNextState< Machine extends StateMachine<any, any>, CurrentState extends keyof Machine, Event extends keyof Machine[CurrentState]> = Machine[CurrentState][Event];
type NextState = GetNextState<TrafficLightMachine, "red", "timer">; // "green"Testing Conditional Types
// Type testing utilitiestype Expect<T extends true> = T;type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
// Test casestype Test1 = Expect<Equal<ReturnType<() => string>, string>>; // ✅type Test2 = Expect<Equal<Parameters<(a: number, b: string) => void>, [number, string]>>; // ✅
// Runtime testingfunction testConditionalTypes() { // These should compile without errors if types are correct const test1: ReturnType<() => string> = "hello"; const test2: Parameters<(a: number) => void> = [42];
console.log("All type tests passed!");}Best Practices
1. Keep Conditional Types Simple
// ❌ Too complex in one typetype ComplexType<T> = T extends string ? T extends `${infer A}_${infer B}` ? A extends "user" ? B extends "create" | "update" ? { action: B; entity: A; data: any } : never : never : never : never;
// ✅ Break into smaller, composable typestype ParseAction<T extends string> = T extends `${infer Entity}_${infer Action}` ? { entity: Entity; action: Action } : never;
type ValidateUserAction<T> = T extends { entity: "user"; action: infer A } ? A extends "create" | "update" ? { action: A; entity: "user"; data: any } : never : never;
type UserActionType<T extends string> = ValidateUserAction<ParseAction<T>>;2. Use Meaningful Names
// ❌ Unclear purposetype Helper<T, U> = T extends U ? T : never;
// ✅ Clear intenttype ExtractMatching<T, Pattern> = T extends Pattern ? T : never;3. Document Complex Types
/** * Extracts the return type of async functions, unwrapping Promise types. * * @example * type Result = AsyncReturnType<() => Promise<string>>; // string * type SyncResult = AsyncReturnType<() => string>; // string */type AsyncReturnType<T extends (...args: any[]) => any> = ReturnType<T> extends Promise<infer U> ? U : ReturnType<T>;Conclusion
Conditional types and type manipulation are the foundation of advanced TypeScript programming. They enable:
- Dynamic type behavior based on input types
- Sophisticated type inference with
infer - String manipulation at the type level
- Recursive type definitions for complex data structures
- Type-safe APIs that adapt to usage patterns
Mastering these concepts allows you to build incredibly powerful and flexible type systems that provide excellent developer experience while maintaining type safety.
Next Steps
In the next part, we’ll explore Mapped Types & Template Literals in greater depth, focusing on advanced mapping techniques and practical applications for building robust type systems.