TypeScript Advanced Type System: Conditional Types & Inference

By Mohammed Sahil KhanMarch 8, 2024
TypeScriptType SystemAdvanced Patterns

TypeScript Advanced Type System: Conditional Types & Inference

TypeScript's type system is more powerful than most developers realize. Beyond basic types and generics lie advanced patterns like conditional types, type inference, and mapped types that enable building robust, self-documenting APIs.

Conditional Types: Type-Level Logic

Conditional types allow you to select types based on conditions, creating polymorphic type logic.

Basic Conditional Type

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

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

Extracting Union Types

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

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

Type Inference with infer

The infer keyword allows you to extract and infer types from complex structures.

// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type MyFunc = (x: number) => string;
type Result = ReturnType<MyFunc>; // string

// Extract Promise resolved value
type Unwrap<T> = T extends Promise<infer U> ? U : T;

type PromiseString = Unwrap<Promise<string>>; // string
type PlainNumber = Unwrap<number>; // number

Mapped Types for Transformations

Mapped types iterate over object keys and transform their values.

// Make all properties readonly
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Make all properties optional
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// Convert all properties to getters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
  name: string;
  age: number;
}

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

Distribution in Conditional Types

Conditional types distribute over union types automatically.

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

type StrOrNum = ToArray<string | number>;
// (string | number)[] - NOT string[] | number[]

// To prevent distribution, wrap in tuples
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;

Advanced Pattern: Deep Partial

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

interface Config {
  app: {
    name: string;
    port: number;
    database: {
      host: string;
      port: number;
    };
  };
}

type DeepPartialConfig = DeepPartial<Config>;

Building Type-Safe Builders

type Builder<T> = {
  [K in keyof T]-?: (value: T[K]) => Builder<Omit<T, K>>;
} & {
  build(): T;
};

function createBuilder<T>(initial: T): Builder<T> {
  // Implementation creates fluent API
  return null as any;
}

const user = createBuilder({ name: '', email: '' })
  .name('John')
  .email('john@example.com')
  .build();

Performance Implications

Complex conditional types can impact compilation time. Use type aliases for recursive patterns sparingly and consider breaking complex type logic into smaller, reusable units.

SEO Keywords

Advanced TypeScript patterns enable building scalable, type-safe applications. Master conditional types, type inference, mapped types, and distribution patterns to write maintainable code that scales with your project.

Mastering TypeScript's advanced type system transforms how you build robust, self-documenting APIs and applications.