Conditional Types

Conditional types are one of the most powerful features in TypeScript's type system. They let you express if/else logic at the type level — computing new types based on the relationships between types. Conditional types are the foundation behind built-in utility types like Exclude, ReturnType, and Parameters, and are essential for writing advanced type-level programs and type-safe libraries.

1. Basic Syntax

Conditional types use a syntax similar to the ternary operator: T extends U ? X : Y. If type T is assignable to type U, the result is X; otherwise it is Y.

Simple Type Check

type IsString<T> = T extends string ? 'yes' : 'no';

type A = IsString<string>;  // 'yes'
type B = IsString<number>;  // 'no'
type C = IsString<'hello'>; // 'yes' — string literal extends string

Nested Conditionals

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<() => void>; // 'function'
type T2 = TypeName<string[]>;  // 'object'

Conditional Types in Constraints

// Only allow arrays — reject non-array types at compile time
type Flatten<T> = T extends any[] ? T[number] : T;

type Str = Flatten<string[]>;   // string
type Num = Flatten<number>;     // number (returned as-is)

2. The infer Keyword

The infer keyword can only be used within the extends clause of a conditional type. It declares a type variable to be inferred, letting TypeScript automatically extract the concrete type from the matched position. This is the core mechanism behind utility types like ReturnType and Parameters.

Extract Array Element Type

type ElementType<T> = T extends (infer E)[] ? E : never;

type Str = ElementType<string[]>;   // string
type Num = ElementType<number[]>;   // number
type Never = ElementType<string>;   // never

Extract Promise Inner Type (Recursive Awaited)

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type A = Awaited<Promise<string>>;              // string
type B = Awaited<Promise<Promise<number>>>;     // number
type C = Awaited<string>;                        // string (non-Promise passthrough)

Extract Function Return Type

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

type A = MyReturnType<() => string>;          // string
type B = MyReturnType<(x: number) => boolean>; // boolean

Extract Function Parameters

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

type A = MyParameters<(x: string, y: number) => void>; // [x: string, y: number]

Extract Constructor Parameters

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

class User {
  constructor(public name: string, public age: number) {}
}
type Args = MyConstructorParameters<typeof User>; // [name: string, age: number]

Extract First Argument Type

type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type A = FirstArg<(x: string, y: number) => void>; // string
type B = FirstArg<() => void>;                      // unknown

3. Distributive Conditional Types

When a conditional type acts on a generic parameter that is a naked type parameter (not wrapped), and a union type is passed in, the conditional type automatically evaluates against each member of the union separately, then combines the results into a new union. This is called distributive behavior.

Distribution in Action

type ToArray<T> = T extends any ? T[] : never;

// Union distributes: ToArray<string> | ToArray<number>
type A = ToArray<string | number>; // string[] | number[]

Preventing Distribution — Tuple Wrapping [T] extends [any]

Wrapping the type parameter in a tuple prevents distributive behavior:

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

// NOT distributed — treated as a single union
type B = ToArrayNonDist<string | number>; // (string | number)[]

Side-by-Side Comparison

Pattern Input Result
T extends any ? T[] : neverstring | numberstring[] | number[]
[T] extends [any] ? T[] : neverstring | number(string | number)[]

4. Built-in Utility Types — How They Work

Many of TypeScript's built-in utility types are implemented using conditional types under the hood. Understanding their implementations helps you write your own.

Exclude<T, U>

Removes from union T all members assignable to U. Uses distributive conditional types.

// Implementation
type Exclude<T, U> = T extends U ? never : T;

// Usage
type A = Exclude<'a' | 'b' | 'c', 'a'>;        // 'b' | 'c'
type B = Exclude<string | number | boolean, string>; // number | boolean

Extract<T, U>

Extracts from union T all members assignable to U. The opposite of Exclude.

// Implementation
type Extract<T, U> = T extends U ? T : never;

// Usage
type A = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type B = Extract<string | number | (() => void), Function>; // () => void

NonNullable<T>

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

// Usage
type A = NonNullable<string | null | undefined>; // string

ReturnType<T>

// Implementation
type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R ? R : any;

// Usage
type A = ReturnType<() => string>;    // string
type B = ReturnType<typeof parseInt>; // number

Parameters<T>

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

// Usage
type A = Parameters<(x: string, y: number) => void>; // [x: string, y: number]

ConstructorParameters<T>

// Implementation
type ConstructorParameters<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: infer P) => any ? P : never;

// Usage
type A = ConstructorParameters<ErrorConstructor>; // [message?: string]

InstanceType<T>

// Implementation
type InstanceType<T extends abstract new (...args: any) => any> =
  T extends abstract new (...args: any) => infer R ? R : any;

// Usage
class User { name = ''; }
type A = InstanceType<typeof User>; // User

Utility Types at a Glance

Utility Type Purpose Technique
Exclude<T, U>Remove union membersdistributive + never
Extract<T, U>Keep union membersdistributive + never
NonNullable<T>Strip null/undefineddistributive + never
ReturnType<T>Extract return typeinfer R
Parameters<T>Extract params tupleinfer P
ConstructorParameters<T>Extract ctor paramsinfer P
InstanceType<T>Extract instance typeinfer R

5. Advanced Patterns

Conditional + Mapped Types: Filter Keys by Value Type

type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}
type StringFields = PickByValue<User, string>; // { name: string; email: string }

Extract Optional and Required Keys

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

interface Config { host: string; port: number; debug?: boolean; }
type Opt = OptionalKeys<Config>;  // 'debug'
type Req = RequiredKeys<Config>;  // 'host' | 'port'

Filter Non-Function Property Keys

type NonFunctionKeys<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

type DataKeys = NonFunctionKeys<{ id: number; greet(): void; name: string }>;
// 'id' | 'name'

Template Literal + Conditional Types

type EventHandler<T extends string> =
  T extends \`on\${infer Event}\` ? Event : never;

type E = EventHandler<'onClick' | 'onFocus' | 'name'>;
// 'Click' | 'Focus'   — 'name' filtered out as never

// Reverse: add 'on' prefix
type AddOnPrefix<T extends string> = \`on\${Capitalize<T>}\`;
type H = AddOnPrefix<'click' | 'focus'>; // 'onClick' | 'onFocus'

Recursive Conditional Types: DeepReadonly

type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface Nested {
  a: { b: { c: string } };
}
type R = DeepReadonly<Nested>;
// { readonly a: { readonly b: { readonly c: string } } }

Recursive Conditional Types: DeepPartial

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  server: { host: string; port: number };
  db: { url: string };
}
type PartialConfig = DeepPartial<Config>;
// All nested fields become optional

Key Remapping with as Clause

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

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

6. Real-world Examples

API Response Type Extraction

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

type UnwrapResponse<T> = T extends ApiResponse<infer D> ? D : never;

type UserData = UnwrapResponse<ApiResponse<{ id: number; name: string }>>;
// { id: number; name: string }

// Unwrap array of API responses
type UnwrapAll<T extends any[]> = {
  [K in keyof T]: T[K] extends ApiResponse<infer D> ? D : T[K];
};

Event Handler Type Inference

type EventMap = {
  click: { x: number; y: number };
  focus: { target: HTMLElement };
  keydown: { key: string; code: string };
};

type EventPayload<T extends keyof EventMap> = EventMap[T];

type Handler<T extends keyof EventMap> = (event: EventPayload<T>) => void;

// Infer event name from handler
type EventFromHandler<T> =
  T extends (event: infer E) => void
    ? E extends EventMap[infer K extends keyof EventMap]
      ? K
      : never
    : never;

Form Validation Types

type FieldValidator<T> = {
  [K in keyof T]: T[K] extends string
    ? { type: 'text'; minLength?: number; maxLength?: number }
    : T[K] extends number
    ? { type: 'number'; min?: number; max?: number }
    : T[K] extends boolean
    ? { type: 'checkbox' }
    : { type: 'custom'; validate: (val: T[K]) => boolean };
};

interface SignupForm {
  username: string;
  age: number;
  agree: boolean;
}
type SignupValidation = FieldValidator<SignupForm>;
// {
//   username: { type: 'text'; minLength?: number; maxLength?: number };
//   age: { type: 'number'; min?: number; max?: number };
//   agree: { type: 'checkbox' };
// }

Path Parameter Extraction (Route Typing)

type ExtractParams<T extends string> =
  T extends \`\${string}:\${infer Param}/\${infer Rest}\`
    ? { [K in Param | keyof ExtractParams<Rest>]: string }
    : T extends \`\${string}:\${infer Param}\`
    ? { [K in Param]: string }
    : {};

type Params = ExtractParams<'/users/:id/posts/:postId'>;
// { id: string; postId: string }

7. FAQ

When should I use conditional types vs mapped types?

Use conditional types for type-level branching (like if/else based on type relationships). Use mapped types to iterate over an object's keys and transform each property. They are often combined — for example, using mapped types to iterate keys while using conditional types to filter or transform value types.

Why does my conditional type return never?

Usually for two reasons: (1) All members of a union were filtered out — distributive conditional types return never for each member, and never | never is just never; (2) The type in the extends clause didn't match, so the false branch returned never.

How to debug complex conditional types?

Break complex conditional types into intermediate type aliases and inspect each step with type Debug = YourType. In VS Code, hover over type aliases to see the expanded type. You can also use type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never; to force expansion.

Can infer be used outside of extends?

No. infer can only appear within the extends clause of a conditional type. It is the type system's pattern matching mechanism and requires the conditional type to provide matching context.

When should I prevent distributive behavior?

When you want to treat a union type as a whole rather than processing each member individually. For example, IsNever<T> — without preventing distribution, IsNever<never> returns never instead of true. Wrap with [T] extends [never] to fix this.

Use the search box below to quickly find conditional type patterns: