TypeScript Patterns You Should Know
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 expected3. 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 number5. 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