tutorial 15 min read · September 12, 2025

Mastering TypeScript

Advanced TypeScript patterns that improve developer experience and prevent bugs in production.

TypeScript’s type system is its most powerful feature. Used well, it eliminates entire categories of bugs and makes refactoring fearless. Used poorly, it’s just noise that slows you down.

Beyond Basic Types

Moving beyond string, number, and boolean unlocks patterns that model real domain logic.

Union Types for Domain Modeling

// Bad: stringly typed, error-prone
function setStatus(status: string) { ... }
setStatus('pending'); // works
setStatus('maybe');   // works - invalid!

// Good: precise, exhaustive
type Status = 'pending' | 'in_progress' | 'completed' | 'cancelled';
function setStatus(status: Status) { ... }
setStatus('pending');  // works
setStatus('maybe');    // Error: Type '"maybe"' is not assignable

Discriminated Unions for State Machines

type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function renderState<T>(state: LoadingState<T>) {
  switch (state.status) {
    case 'idle': return 'Ready';
    case 'loading': return 'Loading...';
    case 'success': return `Data: ${state.data}`; // data is typed!
    case 'error': return `Error: ${state.error.message}`;
  }
}

Generics and Type Inference

Generics enable reusable code without sacrificing type safety. The art is letting TypeScript infer, not forcing it.

Constrain Early, Infer Late

// Bad: overly specific, callers must specify type
function firstString(arr: string[]): string { return arr[0]; }

// Good: generic but constrained
function first<T extends string[]>(arr: T): T[number] { return arr[0]; }

// TypeScript infers from usage
const x = first(['a', 'b', 'c']); // type: string

Conditional Types for Type-Level Logic

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

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

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type C = ElementOf<string[]>;       // string

The satisfies Operator

Validates against a type while preserving literal types:

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
} satisfies Record<string, string | number>;

// config.apiUrl is still 'https://api.example.com' (literal)
// not just string

Branded Types for Type Safety

Prevent mixing up values that share the same primitive type:

type UserId = string & { readonly __brand: 'UserId' };
type PostId = string & { readonly __brand: 'PostId' };

function createUserId(id: string): UserId {
  return id as UserId;
}

// These won't accidentally mix
const userId = createUserId('user-123');
const postId = createUserId('post-456') as PostId;

function getUser(id: UserId) { ... }
getUser(postId); // Error: Argument of type 'PostId' is not assignable to 'UserId'

Utility Types for Common Patterns

// Make specific properties optional
type Partial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

// Example
interface User {
  id: string;
  name: string;
  email: string;
  avatar?: string;
}
// Only allow updating name and avatar
type UpdateUser = Partial<User, 'id' | 'email'>;

// Deep readonly
type DeepReadonly<T> = T extends object
  ? { readonly [P in keyof T]: DeepReadonly<T[P]> }
  : T;

Const Assertions for Exhaustive Patterns

const STATUSES = ['pending', 'active', 'completed'] as const;
type Status = typeof STATUSES[number]; // 'pending' | 'active' | 'completed'

function handleStatus(status: Status) {
  switch (status) {
    case 'pending': return 'Waiting...';
    case 'active': return 'In progress';
    case 'completed': return 'Done';
    // TypeScript knows this is exhaustive
  }
}

Key Takeaways

  1. Union types model domain constraints better than raw strings
  2. Generics should infer from usage, not be forced
  3. Branded types prevent mixing similar primitives
  4. satisfies validates without narrowing literals
  5. Const assertions enable exhaustive switch statements

TypeScript rewards investment. The more you learn the type system, the more bugs it catches before they reach production.