TypeScript Conditional Types

Conditional types allow TypeScript to choose one type or another based on a condition. They follow an if-else pattern at the type level: "if this type extends that type, use type A; otherwise use type B." Conditional types make it possible to write generic utilities that adapt their output type based on their input type.

Basic Syntax

  T extends U ? TypeIfTrue : TypeIfFalse
  |              |              |
  |              |              +-- Type returned when condition is FALSE
  |              +----------------- Type returned when condition is TRUE
  +-------------------------------- Type being tested

This mirrors the JavaScript ternary operator (condition ? a : b), but operates on types instead of values.

Simple Conditional Type

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

type Check1 = IsString<string>;   // "yes"
type Check2 = IsString<number>;   // "no"
type Check3 = IsString<"hello">;  // "yes" — literal string extends string
type Check4 = IsString<boolean>;  // "no"

Conditional Types with Generics

// Returns string when T is number, returns number when T is string
type Flip<T> = T extends number ? string : T extends string ? number : never;

type A = Flip<number>;  // string
type B = Flip<string>;  // number
type C = Flip<boolean>; // never

Distributive Conditional Types

When a conditional type receives a union type, it distributes the condition over each member of the union individually. TypeScript applies the condition to each type in the union and combines the results.

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

type Result1 = ToArray<string>;           // string[]
type Result2 = ToArray<number>;           // number[]
type Result3 = ToArray<string | number>;  // string[] | number[]
//                     ^^^^^^^^^^^^^^
//                     Distributed: string[] | number[]
//                     NOT: (string | number)[]
  Distribution over union:
  T = string | number

  ToArray<string | number>
        ↓ distributes
  ToArray<string> | ToArray<number>
        ↓
  string[] | number[]

Preventing Distribution

Wrapping both sides in square brackets prevents distribution and treats the union as a single type.

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

type R1 = NoDistribute<string | number>; // (string | number)[]

infer Keyword

The infer keyword extracts a type from within another type during the conditional check. It captures part of the matched type and gives it a name for use in the true branch.

  T extends (infer U)[] ? U : never
                  |
                  +-- Capture the element type of T as U
                      If T is string[] → U is string
                      If T is number[] → U is number
// Extract element type from an array type
type ElementType<T> = T extends (infer U)[] ? U : never;

type E1 = ElementType<string[]>;   // string
type E2 = ElementType<number[]>;   // number
type E3 = ElementType<boolean[][]>; // boolean[]
type E4 = ElementType<string>;     // never — not an array
// Extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type F1 = ReturnType<() => string>;          // string
type F2 = ReturnType<(x: number) => boolean>; // boolean
type F3 = ReturnType<() => number[]>;        // number[]

Extracting Promise Resolution Type

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

type R1 = Awaited<Promise<string>>;  // string
type R2 = Awaited<Promise<number>>;  // number
type R3 = Awaited<boolean>;          // boolean (not a Promise — returns as-is)

Excluding Types from a Union

// Built into TypeScript as Exclude<T, U>
type MyExclude<T, U> = T extends U ? never : T;

type T1 = MyExclude<string | number | boolean, string>;
// Result: number | boolean  (string is excluded)

type T2 = MyExclude<"a" | "b" | "c" | "d", "a" | "c">;
// Result: "b" | "d"

Filtering Object Properties by Type

// Keep only properties whose values are of a specified type
type PickByValue<T, ValueType> = {
    [K in keyof T as T[K] extends ValueType ? K : never]: T[K]
};

type Product = {
    id: number;
    name: string;
    price: number;
    available: boolean;
    category: string;
};

type NumberProps = PickByValue<Product, number>;
// { id: number; price: number }

type StringProps = PickByValue<Product, string>;
// { name: string; category: string }

Nested Conditional Types

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

type N1 = TypeName<string>;    // "string"
type N2 = TypeName<number>;    // "number"
type N3 = TypeName<() => void>; // "function"
type N4 = TypeName<{ a: 1 }>;  // "object"

Real-World Utility Pattern

// Make all function properties of an object return Promises
type Promisify<T> = {
    [K in keyof T]: T[K] extends (...args: infer A) => infer R
        ? (...args: A) => Promise<R>
        : T[K];
};

type UserService = {
    getUser(id: number): string;
    getCount(): number;
    appName: string;
};

type AsyncUserService = Promisify<UserService>;
// {
//   getUser(id: number): Promise<string>;
//   getCount(): Promise<number>;
//   appName: string;  ← not a function, unchanged
// }

Summary

ConceptSyntaxPurpose
Basic conditionalT extends U ? A : BChoose type based on condition
DistributionAuto on union typesApply condition to each union member
Prevent distribution[T] extends [U]Treat union as whole
inferT extends F<infer U>Extract a type from within another
Nested ternaryT extends A ? X : T extends B ? Y : ZMultiple type branches

Leave a Comment