Conditional Types

Hard

Conditional types select one of two types based on a condition, similar to a ternary operator but at the type level. The syntax `T extends U ? X : Y` checks if T is assignable to U and resolves to X or Y accordingly. When combined with `infer`, conditional types can extract types from complex structures, enabling powerful type-level programming.

Interactive Visualization

Key Points

  • Syntax: `T extends U ? TrueType : FalseType` — a type-level ternary
  • Conditional types distribute over union types when T is a bare type parameter
  • The `infer` keyword declares an inner type variable to extract sub-types
  • `ReturnType<T>` is implemented as a conditional type with infer
  • Nested conditionals can chain multiple checks for complex logic
  • Use `[T] extends [U]` (wrapped in tuple) to prevent distribution over unions

Code Examples

Basic Conditional Types

// Simple conditional type
type IsString<T> = T extends string ? true : false

type A = IsString<string>   // true
type B = IsString<number>   // false
type C = IsString<'hello'>  // true (literal extends string)

// Distributive behavior over unions
type D = IsString<string | number>  // true | false => boolean

// Prevent distribution with tuple wrapping
type IsStringStrict<T> = [T] extends [string] ? true : false
type E = IsStringStrict<string | number>  // false (union is not string)

// Practical example: non-nullable
type NonNullable<T> = T extends null | undefined ? never : T

type F = NonNullable<string | null | undefined>  // string

Conditional types perform type-level branching. They distribute over unions by default, meaning each member of a union is checked individually. This is how Exclude and Extract work.

The `infer` Keyword

// Extract return type of a function
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never

type Fn = (x: string) => number
type Result = MyReturnType<Fn>  // number

// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never
type Nums = ElementOf<number[]>  // number

// Extract promise value
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T
type Val = Awaited<Promise<Promise<string>>>  // string (recursive!)

// Extract first argument type
type FirstArg<T> = T extends (first: infer F, ...rest: unknown[]) => unknown
  ? F
  : never

type FA = FirstArg<(name: string, age: number) => void>  // string

The `infer` keyword lets you "capture" a type from within a conditional check. It is like pattern matching — TypeScript extracts the type that fits in the infer position.

Advanced Conditional Patterns

// Flatten nested arrays to a specific depth
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T

type Deep = number[][][]
type Flat = Flatten<Deep>  // number

// Type-safe event emitter
type EventHandler<T> =
  T extends void
    ? () => void
    : (data: T) => void

interface Events {
  login: { userId: string }
  logout: void
  error: Error
}

type LoginHandler = EventHandler<Events['login']>
// (data: { userId: string }) => void

type LogoutHandler = EventHandler<Events['logout']>
// () => void

// String manipulation at type level
type CamelToSnake<S extends string> =
  S extends `${infer Head}${infer Tail}`
    ? Tail extends Uncapitalize<Tail>
      ? `${Lowercase<Head>}${CamelToSnake<Tail>}`
      : `${Lowercase<Head>}_${CamelToSnake<Tail>}`
    : S

Conditional types can be recursive and can perform string manipulation at the type level. These patterns are common in library types for things like API response transformation.

Common Mistakes

  • Not understanding distributive behavior — `IsString<string | number>` produces `boolean`, not `false`
  • Forgetting that `infer` only works inside the `extends` clause of a conditional type
  • Creating deeply recursive conditional types that hit TypeScript recursion limits
  • Confusing the type-level ternary with a runtime ternary — conditional types run at compile time only
  • Using conditional types when a simpler mapped type or generic constraint would suffice

Interview Tips

  • Be able to implement ReturnType and Parameters from scratch using infer
  • Explain distributive conditional types and how to prevent distribution
  • Show understanding of infer as "type-level pattern matching"
  • Know when conditional types are overkill vs when they are the right tool

Related Concepts