1. Branded types for IDs
A userId and a postId are both strings, but passing one where the other is expected is always a bug. Branded types catch this at compile time:
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
function getUser(id: UserId) { /* ... */ }
const postId = 'abc' as PostId;
getUser(postId); // Type error
Zero runtime cost. The brand only exists in the type system.
2. Exhaustive switch with never
When you add a new variant to a union, every switch statement that handles it should break at compile time:
type Status = 'active' | 'paused' | 'cancelled';
function label(status: Status): string {
switch (status) {
case 'active': return 'Active';
case 'paused': return 'Paused';
case 'cancelled': return 'Cancelled';
default: {
const _exhaustive: never = status;
return _exhaustive;
}
}
}
Add a new status like 'archived' and TypeScript will error on every switch that does not handle it.
3. Result types instead of thrown exceptions
Exceptions are invisible in the type system. A function that can fail should say so in its return type:
type Result<T, E = Error> =
| { ok: true; data: T }
| { ok: false; error: E };
async function parseConfig(raw: string): Result<Config, 'invalid_json' | 'missing_field'> {
// ...
}
The caller is forced to handle both cases. No forgotten try-catch.
4. Const assertions for configuration objects
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'
The union type stays in sync with the array automatically. Add a role to the array and the type updates. No duplication.
5. Zod schemas as the single source of truth
Define the shape once, derive both the runtime validator and the TypeScript type from it:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
});
type User = z.infer<typeof UserSchema>;
The schema validates at runtime. The type checks at compile time. They can never drift apart.
