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">; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<string>; // true

Real-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 members
type Result = ToArray<string | number>; // string[] | number[]
// To prevent distribution, wrap in a tuple
type 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) => void
type KeyHandler = EventHandler<"keydown">; // (event: KeyboardEvent) => void
// For multiple events
type 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 type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type FuncReturn = ReturnType<() => string>; // string
type AsyncReturn = ReturnType<() => Promise<number>>; // Promise<number>
// Extract parameter types
type 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 type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringArrayElement = ArrayElement<string[]>; // string
// Extract promise value
type PromiseValue<T> = T extends Promise<infer U> ? U : T;
type AsyncValue = PromiseValue<Promise<User>>; // User
type SyncValue = PromiseValue<string>; // string
// Extract object value types
type ValueOf<T> = T extends { [key: string]: infer V } ? V : never;
type UserValues = ValueOf<{ name: string; age: number }>; // string | number
// Extract function from event handler
type HandlerFunction<T> = T extends { handler: infer F } ? F : never;
type Handler = HandlerFunction<{ handler: (x: number) => string }>; // (x: number) => string

Complex Inference: Parsing Function Signatures

// Extract first parameter
type Head<T> = T extends [infer H, ...any[]] ? H : never;
// Extract all but first parameter
type Tail<T> = T extends [any, ...infer T] ? T : never;
// Extract last parameter
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type FirstParam = Head<[string, number, boolean]>; // string
type RestParams = Tail<[string, number, boolean]>; // [number, boolean]
type LastParam = Last<[string, number, boolean]>; // boolean
// Curry function type
type 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) => number

Template 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 letter
type Capitalize<S extends string> = S extends `${infer F}${infer R}`
? `${Uppercase<F>}${R}`
: S;
type CapitalizedHello = Capitalize<"hello">; // "Hello"
// Convert to camelCase
type 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 extension
type 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 extraction
type 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 definition
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
// Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Deep required
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
// Flatten nested objects
type 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 path
type 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">; // string
type UserTheme = GetByPath<User, "profile.settings.theme">; // "light" | "dark"
// Set nested property type by path
type 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 number

Conditional Type Utilities

Building Advanced Utilities

// NonNullable that works with nested properties
type DeepNonNullable<T> = {
[P in keyof T]-?: T[P] extends object ? DeepNonNullable<T[P]> : NonNullable<T[P]>;
};
// Extract function types from an object
type 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 types
type 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 chain
type 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 recursion
type 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 types
type 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 transitions
type 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 utilities
type 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 cases
type Test1 = Expect<Equal<ReturnType<() => string>, string>>; // ✅
type Test2 = Expect<Equal<Parameters<(a: number, b: string) => void>, [number, string]>>; // ✅
// Runtime testing
function 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 type
type 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 types
type 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 purpose
type Helper<T, U> = T extends U ? T : never;
// ✅ Clear intent
type 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.

Share Feedback