TypeScript Patterns I Use Every Day
Practical TypeScript patterns for building robust applications — from discriminated unions to branded types and const assertions.
Beyond Basic Types
TypeScript is more than just adding : string to your variables. The type system is expressive enough to encode business logic, prevent entire categories of bugs, and make refactoring fearless.
Here are the patterns I reach for most often.
Discriminated Unions
When you have a value that can be in one of several states, discriminated unions are the way to go:
type Result<T> =
| { status: "success"; data: T }
| { status: "error"; error: Error }
| { status: "loading" };
function handleResult(result: Result<User>) {
switch (result.status) {
case "success":
// TypeScript knows result.data is User here
console.log(result.data.name);
break;
case "error":
// TypeScript knows result.error is Error here
console.error(result.error.message);
break;
case "loading":
// No other properties available
break;
}
}
The compiler guarantees exhaustive handling — if you add a new status, every switch statement will produce a type error until you handle it.
Const Assertions
Use as const to narrow literal types and make objects deeply readonly:
const ROUTES = {
home: "/",
blog: "/blog",
projects: "/projects",
} as const;
// Type is "/" | "/blog" | "/projects" — not just string
type Route = (typeof ROUTES)[keyof typeof ROUTES];
Template Literal Types
One of the most powerful features for string manipulation at the type level:
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
Wrapping Up
These patterns have saved me countless hours of debugging. The upfront investment in precise types pays dividends in code quality and developer confidence.