Skip to main content
Technology & EngineeringTypescript Patterns119 lines

Conditional Types

Branch at the type level with conditional expressions, infer, and distributive behavior.

Quick Summary26 lines
You are an expert in Conditional Types for writing type-safe TypeScript.

## Key Points

- Wrap `T` in a tuple (`[T] extends [U]`) when you need to prevent distributive behavior over unions.
- Chain conditional types for multi-step transformations rather than nesting deeply in a single expression.
- Use `infer` to extract types from function signatures, promise wrappers, and generic containers instead of requiring callers to pass inner types explicitly.
- Unexpected distribution: a conditional type with a naked type parameter distributes over unions, which can produce surprising results when `never` (the empty union) is passed in.
- Excessive recursion depth in deeply recursive conditional types; TypeScript has a recursion limit (typically around 50 levels).

## Quick Example

```typescript
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;  // true
type B = IsString<42>;       // false
```

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

type C = NonNullable<string | null | undefined>;  // string
```
skilldb get typescript-patterns-skills/Conditional TypesFull skill: 119 lines
Paste into your CLAUDE.md or agent config

Conditional Types — TypeScript Patterns

You are an expert in Conditional Types for writing type-safe TypeScript.

Overview

Conditional types follow the syntax T extends U ? X : Y, choosing between two type branches based on an assignability check. Combined with the infer keyword, they can extract and transform types from complex structures. Use conditional types to build utility types, unwrap containers, and enforce relationships between input and output types.

Core Philosophy

Conditional types bring branching logic to the type level, letting you express "if this type matches that shape, then produce X, otherwise produce Y." This is the foundation of TypeScript's ability to compute types from other types, making the type system a small functional programming language that runs at compile time.

The real power of conditional types emerges with infer. Where basic conditional types choose between branches, infer lets you reach inside a type and extract its components — the return type of a function, the element type of an array, the resolved value of a promise. This means you can write generic utilities that adapt to any input shape without requiring callers to manually specify inner types.

Understanding distributive behavior is essential for using conditional types correctly. When a conditional type receives a union, it distributes the check over each member independently and collects the results back into a union. This is usually what you want — filtering, mapping, and transforming union members — but when it is not, wrapping the type parameter in a tuple disables distribution. Mastering when to distribute and when to prevent it is the key to writing conditional types that behave predictably.

Core Concepts

Basic conditional type:

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;  // true
type B = IsString<42>;       // false

Distributive behavior over unions:

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

type C = NonNullable<string | null | undefined>;  // string

When T is a bare type parameter and receives a union, the conditional distributes over each member independently.

The infer keyword — extracting inner types:

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

Implementation Patterns

Deep readonly:

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

Filtering union members:

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

type Mixed = string | (() => void) | number | ((x: number) => string);
type Fns = ExtractFunctions<Mixed>;
// (() => void) | ((x: number) => string)

Flattening nested promises:

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

type Deep = Promise<Promise<Promise<string>>>;
type Flat = FlattenPromise<Deep>;  // string

Preventing distribution with tuple wrapping:

type IsUnion<T> = [T] extends [infer U]
  ? U extends U
    ? [T] extends [U]
      ? false
      : true
    : never
  : never;

Best Practices

  • Wrap T in a tuple ([T] extends [U]) when you need to prevent distributive behavior over unions.
  • Chain conditional types for multi-step transformations rather than nesting deeply in a single expression.
  • Use infer to extract types from function signatures, promise wrappers, and generic containers instead of requiring callers to pass inner types explicitly.

Common Pitfalls

  • Unexpected distribution: a conditional type with a naked type parameter distributes over unions, which can produce surprising results when never (the empty union) is passed in.
  • Excessive recursion depth in deeply recursive conditional types; TypeScript has a recursion limit (typically around 50 levels).

Anti-Patterns

Using conditional types where a simple generic constraint suffices. If you only need to restrict input types, T extends SomeShape on the generic parameter is clearer than a conditional type that maps non-matching types to never. Reserve conditional types for when you need to transform or branch the output type.

Deeply nesting conditional branches in a single expression. A five-level ternary at the type level is as unreadable as a five-level ternary at the value level. Break complex conditional logic into named intermediate types that each handle one decision.

Ignoring the never input edge case. When never (the empty union) is passed to a distributive conditional type, the result is always never because there are no union members to distribute over. This surprises callers who expect a meaningful fallback; guard against it if your utility type might receive never.

Relying on infer in contravariant positions without understanding the implications. When infer appears in a function parameter position, multiple candidates produce an intersection rather than a union. This is correct behavior but catches developers off guard when they expect union extraction.

Writing recursive conditional types without a termination case. Every recursive conditional type needs a base case that stops the recursion. Without one, the compiler hits its depth limit and produces an opaque error. Always ensure the non-recursive branch is reachable for every valid input.

Install this skill directly: skilldb add typescript-patterns-skills

Get CLI access →