Asher Cohen
Back to posts

TypeScript Conditional Types

Create dynamic, flexible types that adapt based on type conditions

Introduction

Conditional types in TypeScript allow you to create non-uniform type mappings. They're like ternary expressions, but for types instead of values.

Basic Syntax

T extends U ? X : Y

Read as: "If type T is assignable to type U, then the result is type X; otherwise, it's type Y."

Basic Examples

Simple Conditionals

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

type A = IsString<'hello'>; // true
type B = IsString<123>; // false
type C = IsString<string>; // true

Nullable Handling

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

type A = NonNullable<string | null>; // string
type B = NonNullable<number>; // number

Practical Use Cases

Extracting Return Types

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

function getUser() {
  return { id: 1, name: 'John' };
}

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

Extracting Array Element Types

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

type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string

Flattening Nested Arrays

type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<number[][]>; // number[]
type B = Flatten<string[]>; // string
type C = Flatten<number>; // number

Distributive Conditional Types

Conditional types automatically distribute over union types.

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

type A = ToArray<string | number>;
// string[] | number[] (distributes)

// Not: (string | number)[]

Preventing Distribution

Wrap types in tuples to prevent distribution:

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

type A = ToArrayNonDistributive<string | number>;
// (string | number)[] (doesn't distribute)

Advanced Patterns

Type Extraction with infer

The infer keyword extracts types from complex structures.

// Extract Promise resolution type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number

// Extract function parameters
type FirstParameter<T> = T extends (arg1: infer P, ...args: any[]) => any
  ? P
  : never;

type A = FirstParameter<(name: string, age: number) => void>; // string

Recursive Conditional Types

// Remove all nulls from nested object
type RemoveNull<T> = {
  [P in keyof T]: T[P] extends null
    ? never
    : T[P] extends object
      ? RemoveNull<T[P]>
      : T[P];
};

interface User {
  id: number;
  name: string | null;
  address: {
    street: string;
    city: string | null;
  };
}

type CleanUser = RemoveNull<User>;
// { id: number; name: string; address: { street: string; city: string; } }

Chaining Conditionals

type Simplify<T> =
  T extends Array<infer U>
    ? Array<Simplify<U>>
    : T extends object
      ? { [K in keyof T]: Simplify<T[K]> }
      : T;

Built-in Conditional Types

TypeScript includes several conditional types in its standard library:

  • Exclude<T, U> - Exclude types from a union
  • Extract<T, U> - Extract types from a union
  • NonNullable<T> - Remove null and undefined
  • ReturnType<T> - Get function return type
  • InstanceType<T> - Get instance type from constructor
  • Parameters<T> - Get function parameter types

Common Pitfalls

Over-Engineering

// ❌ Too complex
type ComplexType<T> = T extends infer U
  ? U extends (...args: any[]) => any
    ? ReturnType<U>
    : U
  : never;

// ✅ Simpler alternative
type SimpleType<T> = T extends Function ? ReturnType<T> : T;

Losing Type Information

// ❌ Loses specificity
type Lossy<T> = T extends object ? any : T;

// ✅ Preserves information
type Safe<T> = T extends object ? Record<string, unknown> : T;

Infinite Recursion

// ❌ Infinite loop
type Infinite<T> = T extends object ? Infinite<T> : T;

// ✅ Add termination
type Safe<T, Depth extends number = 5> = Depth extends 0
  ? T
  : T extends object
    ? Safe<T, Prev[Depth]>
    : T;

Best Practices

Start Simple

Use conditional types only when simpler alternatives don't work.

Document Complex Types

Add comments explaining what complex conditional types do.

Test Your Types

Use type tests to verify conditional types work as expected:

type AssertEqual<T, Expected> = [T] extends [Expected]
  ? [Expected] extends [T]
    ? true
    : false
  : false;

type Test = AssertEqual<ReturnType<() => string>, string>; // true

Use infer Sparingly

Only use infer when you need to extract types from complex structures.

Conclusion

Conditional types unlock powerful type-level programming in TypeScript. Use them wisely to create flexible, reusable type utilities while maintaining readability.

#typescript #types #advanced-types