TypeScript has evolved massively. Here are 5 patterns I use daily that make my code bulletproof.
1. Discriminated Unions for State Management
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; error: string };
function handleState(state: State) {
switch (state.status) {
case "success":
return state.data; // TS knows data exists here
case "error":
return state.error; // TS knows error exists here
}
}
The compiler narrows the type automatically. No more if (data !== undefined) everywhere.
2. satisfies for Type-Safe Configs
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.apiUrl.toUpperCase(); // ✅ Works!
satisfies validates the type without widening it.
3. Template Literal Types for API Routes
type ApiRoute = `/api/${string}`;
type UserRoute = `/api/users/${number}`;
function fetchApi(route: ApiRoute) { /* ... */ }
fetchApi("/api/users/123"); // ✅
fetchApi("/dashboard"); // ❌ Type error
4. Const Assertions for Readonly Everything
const ROLES = ["admin", "user", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "user" | "viewer"
// Instead of: type Role = string
5. Branded Types for Domain Safety
type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };
function getUser(id: UserId) { /* ... */ }
function getPost(id: PostId) { /* ... */ }
const userId = "abc" as UserId;
getUser(userId); // ✅
getPost(userId); // ❌ Type error — can't mix IDs
Which TypeScript patterns do you use the most? Drop your favorites below!
Follow me for more TypeScript and AI content: @tahosin
Top comments (0)