Why TypeScript Patterns Matter
TypeScript is not just about adding type annotations. Used well, it catches entire categories of bugs at compile time, makes refactoring safe, and serves as documentation that is always up to date. These patterns have saved us from real production bugs.
1. Discriminated Unions for State
Instead of multiple boolean flags that can be in impossible states, use a discriminated union:
// ❌ Allows impossible states
type State = {
isLoading: boolean;
data: User | null;
error: string | null;
};
// ✅ Only valid states are representable
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; message: string };
2. Template Literal Types
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
type CSSVariable = `--${string}`;
const color: CSSVariable = "--primary-color"; // valid
const bad: CSSVariable = "primary-color"; // TypeScript error
3. Exhaustive Switch with never
function getStatusLabel(status: "pending" | "active" | "cancelled"): string {
switch (status) {
case "pending": return "Waiting";
case "active": return "Active";
case "cancelled": return "Cancelled";
default:
const _exhaustive: never = status; // compile error if case is missing
throw new Error(`Unhandled status: ${_exhaustive}`);
}
}
4. Generic Component Props
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (item: T) => string;
getValue: (item: T) => string;
}
function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
return (
<select value={getValue(value)} onChange={e => {
const selected = options.find(o => getValue(o) === e.target.value);
if (selected) onChange(selected);
}}>
{options.map(opt => (
<option key={getValue(opt)} value={getValue(opt)}>{getLabel(opt)}</option>
))}
</select>
);
}
5. Const Assertions for Config Objects
const ROUTES = {
home: "/",
blog: "/blog",
admin: "/admin",
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// "/" | "/blog" | "/admin"
// Adding a new route makes it automatically available in the type
6. Mapped Types for Form Errors
interface SignupForm {
email: string;
password: string;
name: string;
}
type FormErrors = Partial<Record<keyof SignupForm, string>>;
// { email?: string; password?: string; name?: string }
// Automatically stays in sync with the form interface
7. Branded Types for IDs
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): User { ... }
const postId = createPostId("post_123");
getUser(postId); // TypeScript error — wrong brand!
8. Conditional Types for API Responses
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const user = await db.users.findById(id);
return { success: true, data: user };
} catch (e) {
return { success: false, error: "User not found" };
}
}
// Usage — TypeScript enforces the check
const result = await fetchUser("123");
if (result.success) {
console.log(result.data.name); // type-safe access
} else {
console.error(result.error);
}
9. Strict Event Handler Typing
// Instead of any in event handlers
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setValue(e.target.value);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
// ...
}
// For custom events with data
type CustomEvent<T> = { detail: T };
function handleCustom(e: CustomEvent<{ userId: string }>) {
console.log(e.detail.userId);
}
10. Utility Type Combinations
interface User {
id: string;
email: string;
name: string;
role: "admin" | "user";
createdAt: Date;
}
// Form for creating a user (no id or createdAt, role defaults)
type CreateUserInput = Omit<User, "id" | "createdAt" | "role"> & {
role?: User["role"];
};
// For updating — all fields optional except id
type UpdateUserInput = Required<Pick<User, "id">> & Partial<Omit<User, "id">>;
These patterns are not just syntactic tricks — they encode domain rules into the type system, turning potential runtime bugs into compile-time errors that you catch before they reach production.
