TypeScript’s type system is Turing-complete. Use it to make invalid states unrepresentable and catch errors at compile time, not in production.
Discriminated unions
// Model state machines with types
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function renderState<T>(state: RequestState<T>) {
switch (state.status) {
case 'idle': return <Placeholder />;
case 'loading': return <Spinner />;
case 'success': return <Data data={state.data} />;
case 'error': return <ErrorMessage error={state.error} />;
// TypeScript knows all cases are handled — no default needed
}
}
Branded types
// Prevent mixing incompatible values
type UserId = string & { readonly __brand: 'UserId' };
type PostId = string & { readonly __brand: 'PostId' };
function UserId(id: string): UserId { return id as UserId; }
function PostId(id: string): PostId { return id as PostId; }
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = UserId('user-123');
const postId = PostId('post-456');
getUser(userId); // ✅ Works
getUser(postId); // ❌ Type error — PostId is not UserId
Template literal types
// Type-safe route parameters
type Route = `/${string}`;
type ApiRoute = `/api/${string}`;
type IdRoute = `/${string}/${string}`;
function route<T extends Route>(path: T): T { return path; }
route('/users'); // ✅
route('/api/users'); // ✅
route('users'); // ❌ Missing leading slash
// Type-safe event names
type EventName = `${'click' | 'hover' | 'focus'}-${'start' | 'end'}`;
const event: EventName = 'click-start'; // ✅
const bad: EventName = 'click'; // ❌
Type guards
// Narrow types with type predicates
function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Custom type guard for API responses
interface ApiResponse {
data: unknown;
status: number;
}
function isSuccessResponse(response: ApiResponse): response is ApiResponse & { data: Record<string, unknown> } {
return response.status >= 200 && response.status < 300;
}
// Usage
const response = await fetch('/api/data');
const json: ApiResponse = await response.json();
if (isSuccessResponse(json)) {
// json.data is now Record<string, unknown>
console.log(json.data.users);
}
Runtime validation with Zod
import { z } from 'zod';
// Define schemas that match your types
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['user', 'admin']),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
// Validate at boundaries
function parseUser(data: unknown): User {
return UserSchema.parse(data);
}
// Safe parsing that doesn't throw
function safeParseUser(data: unknown) {
const result = UserSchema.safeParse(data);
if (!result.success) {
console.error('Validation errors:', result.error.flatten());
return null;
}
return result.data;
}
Mapped types
// Make all properties optional and nullable
type Nullable<T> = { [K in keyof T]: T[K] | null };
// Make specific properties required
type RequireFields<T, K extends keyof T> = T & { [P in K]-?: T[P] };
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// Extract function parameter types
type ParamsOf<T> = T extends (...args: infer P) => any ? P : never;
Conditional types
// Extract return type of async functions
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never;
// Check if type has a specific property
type HasId<T> = T extends { id: any } ? true : false;
// Distribute conditional types over unions
type NonNullableArray<T> = T extends Array<infer E> ? NonNullable<E>[] : T;
Anti-patterns
- Don’t use
any— useunknownand narrow with type guards - Don’t use type assertions (
as) to silence the compiler — fix the types - Don’t ignore
strictNullChecks— it catches 15% of bugs - Don’t use
enum— useconstobjects withas const - Don’t skip input validation — TypeScript types are erased at runtime
When it triggers
- TypeScript type patterns
- advanced TypeScript
- type-safe code
- discriminated unions
- runtime type validation