TypeScript Patterns I Use Every Day
Practical TypeScript patterns from daily production work that improve code quality, cut bugs, and make codebases easier to maintain.
Published , typescript, patterns, best-practices, frontend, react
TypeScript’s value is not just catching typos. Used well, it encodes business rules, documents intent, and eliminates entire categories of bugs before they reach production. These are the patterns I reach for most in professional work.
Discriminated Unions for State
Avoid the “impossible state” problem by making illegal states unrepresentable.
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
Now you cannot accidentally read data when the status is error. TypeScript will catch it. Compare that with the common pattern of { isLoading, isError, data, error } booleans, where it is entirely possible to have isLoading: true and isError: true at the same time, which means nothing.
Branded Types for Domain Primitives
When two values have the same underlying type but different semantics, brand them.
type UserId = string & { readonly _brand: 'UserId' };
type ProductId = string & { readonly _brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
Now you cannot accidentally pass a ProductId where a UserId is expected, even though both are strings at runtime. This catches a surprisingly common class of bug in codebases that deal with multiple entity types.
satisfies for Config Objects
The satisfies operator validates a value against a type without widening it.
const routes = {
home: '/',
about: '/about',
portfolio: '/portfolio',
} satisfies Record<string, string>;
// routes.home is typed as '/' (literal), not string
Without satisfies, you would have to choose between type safety (which widens to string) and inference (which loses the type check). satisfies gives you both.
as const + Type Inference from Values
Define your data once and derive types from it.
const STATUSES = ['pending', 'active', 'cancelled'] as const;
type Status = typeof STATUSES[number]; // 'pending' | 'active' | 'cancelled'
This pattern ensures your type and your runtime array stay in sync automatically. If you add a new status to the array, the type updates. If you make a typo in the type, the compiler catches it.
Exhaustive Checks in Switch Statements
function assertNever(value: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(value)}`);
}
function handleStatus(status: Status): string {
switch (status) {
case 'pending': return 'Waiting...';
case 'active': return 'Running';
case 'cancelled': return 'Stopped';
default: return assertNever(status);
}
}
When you add a new Status value later and forget to update this switch, the TypeScript compiler gives you a type error at the assertNever call. This is exhaustive checking: the compiler proves every case is handled.
Utility Types in Practice
A few I use constantly:
// Pick only what you need
type UserPreview = Pick<User, 'id' | 'name' | 'avatarUrl'>;
// Make all fields optional for partial updates
type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;
// Readonly for immutable data
type Config = Readonly<AppConfig>;
// Record for dictionaries
type StatusColors = Record<Status, string>;
Partial<Pick<T, K>> is especially useful for update payloads. You get type safety on which fields can be updated without making all of them required.
Generic Components in React
Type your component props generically when they accept arbitrary data shapes.
interface ListProps<T> {
items: T[];
getKey: (item: T) => string;
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, getKey, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
This gives you a fully type-safe generic list component. TypeScript infers T from the items prop, so the consumer does not need to specify it.
Template Literal Types for String Contracts
When your strings follow a pattern, encode the pattern in the type.
type EventName = `on${Capitalize<string>}`;
type CSSVariable = `--${string}`;
type Route = `/${string}`;
Useful for design system tokens, event systems, and API route definitions.
The Principle Behind All of These
Every pattern above does one thing: it moves a potential runtime error to a compile-time error. Runtime errors reach your users. Compile-time errors reach you. The point of TypeScript is not type annotations. It is closing the gap between writing code and knowing it is correct.