blog

5 TypeScript Patterns That Changed How I Code

Practical TypeScript patterns for writing more maintainable and type-safe code

January 5, 2025

typescriptjavascriptprogramming

5 TypeScript Patterns That Changed How I Code

After two years of TypeScript, these patterns have become essential to how I write code.

1. Discriminated Unions for State

Instead of optional properties everywhere:

// ❌ Hard to reason about
interface LoadingState {
  isLoading: boolean;
  data?: User[];
  error?: string;
}

Use discriminated unions:

// ✅ Impossible states are impossible
type DataState =
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: string };

Now TypeScript knows when data exists:

if (state.status === 'success') {
  // TypeScript knows state.data exists here!
  state.data.forEach(user => ...)
}

2. Branded Types for IDs

Prevent mixing up different IDs:

type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };

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

const userId: UserId = 'user-123' as UserId;
const postId: PostId = 'post-456' as PostId;

getUser(postId); // ❌ TypeScript error!

No more accidentally passing the wrong ID type.

3. Const Assertions for Better Inference

// ❌ Type is string[]
const colors = ['red', 'blue', 'green'];

// ✅ Type is readonly ['red', 'blue', 'green']
const colors = ['red', 'blue', 'green'] as const;

// Now you can do:
type Color = typeof colors[number]; // 'red' | 'blue' | 'green'

4. Generic Constraints for Flexible APIs

function sortBy<T, K extends keyof T>(
  items: T[],
  key: K
): T[] {
  return items.sort((a, b) =>
    a[key] > b[key] ? 1 : -1
  );
}

const users = [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 }
];

sortBy(users, 'age'); // ✅ Works!
sortBy(users, 'email'); // ❌ TypeScript error - no 'email' property

5. Type Guards for Runtime Safety

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

// Now you can safely narrow types:
function processData(data: unknown) {
  if (isUser(data)) {
    // TypeScript knows data is User here
    console.log(data.name);
  }
}

Bonus: Utility Types

Don’t sleep on built-in utility types:

// Pick only certain properties
type UserPreview = Pick<User, 'id' | 'name'>;

// Make all properties optional
type PartialUser = Partial<User>;

// Make all properties required
type RequiredUser = Required<User>;

// Exclude certain keys
type UserWithoutPassword = Omit<User, 'password'>;

Conclusion

TypeScript is more than just adding types to JavaScript. These patterns help you write code that’s:

  • Safer (impossible states are impossible)
  • More maintainable (types document intent)
  • Easier to refactor (compiler catches breaks)

Start using one pattern this week. Your future self will thank you.

← Back to all writing