logo
HomeAboutSkillsProjectsExperienceBlogContact
© 2026 Sarun Maharjan.
Theme:
Back to all posts
TypeScriptPatternsBest Practices

TypeScript Patterns I Use Daily

January 28, 2026

TypeScript's type system is incredibly powerful, but most codebases only scratch the surface. Here are the patterns I reach for every single day.

Discriminated Unions for State

Instead of using boolean flags to represent state, use discriminated unions:

// Instead of this
type Request = {
  isLoading: boolean;
  isError: boolean;
  data?: User;
  error?: Error;
};

// Use this
type Request =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: Error };

The discriminated union makes impossible states impossible. You can't accidentally access data when the request is still loading.

Branded Types for Type Safety

Primitive types like string and number lose meaning in large codebases. Branded types add semantic meaning:

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

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

function getUser(id: UserId): Promise<User> {
  // Now you can't accidentally pass an OrderId here
}

The satisfies Operator

The satisfies operator (TypeScript 4.9+) lets you validate types while preserving the narrowest type:

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

// config.apiUrl is still typed as string, not string | number

Type-Safe Event Emitters

Using generics to create type-safe event systems:

type EventMap = {
  'user:login': { userId: string; timestamp: number };
  'user:logout': { userId: string };
  'error': { message: string; code: number };
};

class TypedEmitter<T extends Record<string, unknown>> {
  private handlers = new Map<keyof T, Set<Function>>();

  on<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
  }

  emit<K extends keyof T>(event: K, payload: T[K]) {
    this.handlers.get(event)?.forEach(handler => handler(payload));
  }
}

const emitter = new TypedEmitter<EventMap>();
emitter.on('user:login', ({ userId, timestamp }) => {
  // Fully typed!
});

Const Assertions for Literal Types

Use as const to preserve literal types in arrays and objects:

const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'

Template Literal Types

Create complex string types using template literals:

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIRoute = `/api/${string}`;
type Endpoint = `${HTTPMethod} ${APIRoute}`;

// Valid: "GET /api/users"
// Invalid: "PATCH /api/users"

Conclusion

These patterns have saved me countless hours of debugging. The key insight is that TypeScript's type system can encode business rules, making bugs literally impossible to write. Invest time in learning these patterns — your future self will thank you.