Back to Garden
Article

TypeScript Patterns I Use Daily

Practical TypeScript patterns that make code more maintainable, readable, and type-safe.

Peter
#typescript#patterns#best-practices

TypeScript Patterns I Use Daily

After years of writing TypeScript, these are the patterns I reach for most often.

1. Discriminated Unions

Perfect for state management and API responses:

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

function handleState<T>(state: LoadingState<T>) {
  switch (state.status) {
    case 'idle':
      return 'Ready to load';
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Got: ${state.data}`;
    case 'error':
      return `Error: ${state.error.message}`;
  }
}

2. Type Predicates

For narrowing types in a reusable way:

interface User {
  id: string;
  name: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'name' in value
  );
}

// Usage
const maybeUser: unknown = fetchData();
if (isUser(maybeUser)) {
  console.log(maybeUser.name); // TypeScript knows it's a User
}

3. Const Assertions

For literal types without type annotations:

const ROUTES = {
  home: '/',
  about: '/about',
  work: '/work',
} as const;

type Route = typeof ROUTES[keyof typeof ROUTES];
// Type is '/' | '/about' | '/work'

4. Template Literal Types

For string patterns:

type EventName = `on${Capitalize<string>}`;
type CSSUnit = `${number}${'px' | 'em' | 'rem' | '%'}`;

const margin: CSSUnit = '16px'; // ✓
const padding: CSSUnit = '1.5rem'; // ✓

5. Branded Types

For type-safe IDs:

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

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

function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }

const userId = createUserId('123');
getUser(userId); // ✓
// getPost(userId); // ✗ Type error!

Conclusion

These patterns have saved me countless hours of debugging and made my code more self-documenting. The key is knowing when to apply them—not every situation needs advanced types.