codeintelligently
Back to posts

TypeScript Patterns You Should Know

Vaibhav Verma
3 min read

TypeScript Patterns You Should Know

TypeScript's type system is incredibly powerful, but many developers only scratch the surface. Here are patterns that will level up your code.

1. Discriminated Unions

Instead of optional properties, use a discriminant field:

typescript
// Bad: lots of optional properties
interface ApiResponse {
  data?: User;
  error?: string;
  loading?: boolean;
}

// Good: discriminated union
type ApiResponse =
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: string };

function handleResponse(response: ApiResponse) {
  switch (response.status) {
    case 'loading':
      return <Spinner />;
    case 'success':
      return <UserCard user={response.data} />; // data is typed!
    case 'error':
      return <ErrorBanner message={response.error} />; // error is typed!
  }
}

2. Branded Types

Prevent mixing up values that share the same underlying type:

typescript
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('abc123');
const postId = createPostId('xyz789');

getUser(userId); // OK
getUser(postId); // Type error! Can't pass PostId where UserId expected

3. Exhaustive Checks

Ensure you handle every case in a union:

typescript
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

type Shape = 'circle' | 'square' | 'triangle';

function getArea(shape: Shape): number {
  switch (shape) {
    case 'circle': return Math.PI * r * r;
    case 'square': return s * s;
    case 'triangle': return 0.5 * b * h;
    default: return assertNever(shape);
    // If you add 'hexagon' to Shape, this line will error
    // until you handle it — at compile time, not runtime
  }
}

4. The satisfies Operator

Get type checking without losing type inference:

typescript
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)
// config.timeout is still typed as number

5. Template Literal Types

Create precise string types:

typescript
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `/api/${string}`;
type Endpoint = `${HttpMethod} ${ApiRoute}`;

// Valid: "GET /api/users", "POST /api/posts"
// Invalid: "PATCH /api/users", "GET /users"

These patterns aren't just clever — they catch real bugs at compile time that would otherwise slip through to production.

Explore by topic