Skip to main content

What are Conditional Types?

Conditional types select one of two possible types based on a condition expressed as a type relationship test. They follow the pattern T extends U ? X : Y and enable powerful type-level programming.
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
Think of conditional types as the ternary operator (? :) but for types instead of values. They enable:
  • Type-level logic and branching
  • Type transformations based on structure
  • Extracting types from complex structures
  • Creating adaptive utility types

Basic Conditional Type Syntax

Simple Condition

type TypeName<T> = 
    T extends string ? "string" :
    T extends number ? "number" :
    T extends boolean ? "boolean" :
    T extends undefined ? "undefined" :
    T extends Function ? "function" :
    "object";

type T0 = TypeName<string>;     // "string"
type T1 = TypeName<"hello">;    // "string"
type T2 = TypeName<42>;         // "number"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>;   // "object"

Conditional with Union Types

From Exercise 10, here’s a practical example:
type ApiResponse<T> = 
    | { status: 'success'; data: T }
    | { status: 'error'; error: string };

// Extract success response type
type SuccessResponse<T> = ApiResponse<T> extends { status: 'success'; data: infer D } 
    ? { status: 'success'; data: D }
    : never;

// Extract error response type
type ErrorResponse<T> = ApiResponse<T> extends { status: 'error'; error: infer E }
    ? { status: 'error'; error: E }
    : never;

The infer Keyword

The infer keyword lets you extract and bind types within conditional types:

Inferring Return Types

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
    return { name: 'John', age: 30 };
}

type User = ReturnType<typeof getUser>;
// { name: string; age: number }

Inferring Parameter Types

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

function createUser(name: string, age: number) {
    return { name, age };
}

type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]

type FirstParam = CreateUserParams[0];   // string
type SecondParam = CreateUserParams[1];  // number

Inferring Array Element Types

type Unpack<T> = T extends (infer U)[] ? U : T;

type StringArray = Unpack<string[]>;  // string
type NumberArray = Unpack<number[]>;  // number
type NotArray = Unpack<string>;       // string
The infer keyword is powerful for:
  • Extracting nested types
  • Unwrapping generic types
  • Building custom utility types
  • Type introspection

Inferring from Complex Structures

From Exercise 10:
type ApiResponse<T> = 
    | { status: 'success'; data: T }
    | { status: 'error'; error: string };

type CallbackBasedAsyncFunction<T> = (
    callback: (response: ApiResponse<T>) => void
) => void;

// Extract the data type T from the callback structure
type ExtractCallbackData<F> = 
    F extends (callback: (response: ApiResponse<infer T>) => void) => void 
        ? T 
        : never;

type OldRequestUsers = (callback: (response: ApiResponse<User[]>) => void) => void;
type UsersData = ExtractCallbackData<OldRequestUsers>;  // User[]

Distributive Conditional Types

When conditional types are applied to union types, they distribute over the union:
type ToArray<T> = T extends any ? T[] : never;

// Distributes over the union
type StringOrNumberArray = ToArray<string | number>;
// string[] | number[] (not (string | number)[])

Understanding Distribution

// Naked type parameter - distributes
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// Distributes to:
// ToArray<string> | ToArray<number>
// string[] | number[]

Practical Distribution Example

// Extract only specific types from a union
type ExtractString<T> = T extends string ? T : never;

type Mixed = string | number | boolean | string[];
type OnlyStrings = ExtractString<Mixed>;
// string (other types become never and are removed)

// Filter out specific types
type RemoveString<T> = T extends string ? never : T;

type WithoutStrings = RemoveString<Mixed>;
// number | boolean | string[]
Distribution is TypeScript’s way of applying the conditional type to each member of a union individually, then combining the results. This is useful for filtering and transforming unions.To prevent distribution, wrap the type parameter in a tuple:
type NoDistribute<T> = [T] extends [any] ? T : never;

Built-in Conditional Types

TypeScript provides several built-in conditional types:

Exclude and Extract

// Exclude - removes types from a union
type Exclude<T, U> = T extends U ? never : T;

type PersonType = 'user' | 'admin' | 'guest';
type ActiveType = Exclude<PersonType, 'guest'>;
// 'user' | 'admin'

// Extract - keeps only matching types
type Extract<T, U> = T extends U ? T : never;

type PrivilegedType = Extract<PersonType, 'user' | 'admin'>;
// 'user' | 'admin'

NonNullable

type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// User

Awaited

From promises:
type Awaited<T> = 
    T extends PromiseLike<infer U> 
        ? Awaited<U>  // Recursive for nested Promises
        : T;

type AsyncUser = Promise<User>;
type SyncUser = Awaited<AsyncUser>;  // User

type NestedAsync = Promise<Promise<User>>;
type Resolved = Awaited<NestedAsync>;  // User

Advanced Patterns

Recursive Conditional Types

// Flatten nested arrays to any depth
type Flatten<T> = T extends Array<infer U> 
    ? Flatten<U>  // Recursively flatten
    : T;

type NestedArray = number[][][];
type Flat = Flatten<NestedArray>;  // number

type MixedNested = (string | number[])[];
type MixedFlat = Flatten<MixedNested>;  // string | number

Mapped Types with Conditional Types

From Exercise 15:
// Remove readonly modifier conditionally
type Mutable<T> = {
    -readonly [P in keyof T]: T[P];
};

// Make properties optional if they're nullable
type NullableToOptional<T> = {
    [P in keyof T as T[P] extends null | undefined ? P : never]?: T[P];
} & {
    [P in keyof T as T[P] extends null | undefined ? never : P]: T[P];
};

interface User {
    name: string;
    age: number;
    email: string | null;
}

type OptionalNullable = NullableToOptional<User>;
// {
//     name: string;
//     age: number;
//     email?: string | null;
// }

Function Overload Resolution

From Exercise 6:
// Conditional types to determine return type based on input
type FilterResult<T extends string> = 
    T extends 'user' ? User[] :
    T extends 'admin' ? Admin[] :
    Person[];

function filterPersons<T extends string>(
    persons: Person[],
    personType: T,
    criteria: Partial<Person>
): FilterResult<T> {
    // Implementation
    return persons.filter(p => p.type === personType) as FilterResult<T>;
}

const users = filterPersons(persons, 'user', { age: 23 });    // User[]
const admins = filterPersons(persons, 'admin', { age: 23 });  // Admin[]

Template Literal Type Transformations

// Capitalize property names
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
    name: string;
    age: number;
}

type UserGetters = Getters<User>;
// {
//     getName: () => string;
//     getAge: () => number;
// }

Type-Level Programming Patterns

Pattern Matching

// Match specific patterns and extract information
type ParseRoute<T extends string> = 
    T extends `${infer Start}/user/${infer UserId}/${infer Rest}`
        ? { start: Start; userId: UserId; rest: Rest }
    : T extends `${infer Start}/user/${infer UserId}`
        ? { start: Start; userId: UserId; rest: never }
    : never;

type Route1 = ParseRoute<"/api/user/123/posts">;
// { start: "/api"; userId: "123"; rest: "posts" }

type Route2 = ParseRoute<"/api/user/456">;
// { start: "/api"; userId: "456"; rest: never }

Type-Safe Event Handlers

interface EventMap {
    click: MouseEvent;
    keypress: KeyboardEvent;
    custom: { data: string };
}

type EventHandler<K extends keyof EventMap> = 
    (event: EventMap[K]) => void;

function addEventListener<K extends keyof EventMap>(
    event: K,
    handler: EventHandler<K>
) {
    // Implementation
}

// Type-safe event handlers
addEventListener('click', (e) => {
    console.log(e.clientX);  // MouseEvent properties available
});

addEventListener('keypress', (e) => {
    console.log(e.key);  // KeyboardEvent properties available
});

Conditional Property Types

From Exercise 8:
// Create a PowerUser that combines User and Admin properties
type PowerUser = Omit<User, 'type'> & Omit<Admin, 'type'> & {
    type: 'powerUser';
};

// Generic version using conditional types
type Merge<T, U> = {
    [K in keyof T | keyof U]: 
        K extends keyof T 
            ? K extends keyof U 
                ? T[K] | U[K]  // If in both, union the types
                : T[K]          // If only in T
            : K extends keyof U 
                ? U[K]          // If only in U
                : never;
};

Practical Use Cases

API Response Handling

type ApiResponse<T> = 
    | { status: 'success'; data: T }
    | { status: 'error'; error: string };

// Extract data type safely
type ExtractData<T> = T extends { data: infer D } ? D : never;

type ResponseData = ExtractData<{ status: 'success'; data: User[] }>;
// User[]

// Check if response is successful
type IsSuccess<T> = T extends { status: 'success' } ? true : false;

type Check1 = IsSuccess<{ status: 'success'; data: User[] }>;  // true
type Check2 = IsSuccess<{ status: 'error'; error: string }>;   // false

Form Validation Types

// Make fields required if they're used in validation
type ValidationRules<T> = {
    [K in keyof T]?: {
        required?: boolean;
        minLength?: number;
        maxLength?: number;
    };
};

type RequiredFields<T, Rules extends ValidationRules<T>> = {
    [K in keyof T as Rules[K] extends { required: true } ? K : never]: T[K];
} & {
    [K in keyof T as Rules[K] extends { required: true } ? never : K]?: T[K];
};

interface UserForm {
    name: string;
    email: string;
    age: number;
}

type Rules = {
    name: { required: true; minLength: 3 };
    email: { required: true };
    age: { required: false };
};

type ValidatedForm = RequiredFields<UserForm, Rules>;
// {
//     name: string;    // Required
//     email: string;   // Required
//     age?: number;    // Optional
// }

Best Practices

Complex nested conditionals are hard to understand:
// Less readable
type Complex<T> = T extends A ? B : T extends C ? D : T extends E ? F : G;

// More readable - break into smaller types
type IsA<T> = T extends A ? B : T;
type IsC<T> = T extends C ? D : T;
type IsE<T> = T extends E ? F : T;
type Simple<T> = IsE<IsC<IsA<T>>>;
infer is for extracting existing types:
// Good - extracting return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Bad - trying to create new types
type Bad<T> = T extends any ? infer U : never;  // U is not defined
Wrap type parameters to prevent distribution:
// Distributes over unions
type Distributed<T> = T extends any ? T[] : never;
type Result1 = Distributed<string | number>;  // string[] | number[]

// Doesn't distribute
type NotDistributed<T> = [T] extends [any] ? T[] : never;
type Result2 = NotDistributed<string | number>;  // (string | number)[]
Add JSDoc comments to explain logic:
/**
 * Extracts the element type from an array type.
 * If T is not an array, returns T unchanged.
 * 
 * @example
 * type A = Unpack<string[]>;  // string
 * type B = Unpack<number>;    // number
 */
type Unpack<T> = T extends (infer U)[] ? U : T;

Generics

Use conditional types with generic parameters

Utility Types

See how built-in utilities use conditional types

Exercise 10

Practice with callback and promise type transformations

Exercise 14

Build complex conditional type patterns

Further Reading

Build docs developers (and LLMs) love