TypeScript Patterns I Use Every Day (And Why They Matter)
2026-05-02 · 6 min read
TypeScript's type system is one of the most expressive in any mainstream language. Most tutorials cover the basics. Here are the patterns I reach for constantly that go slightly deeper.
Discriminated unions for state machines
Any time you have a value that can be in multiple states with different data per state, a discriminated union is cleaner than a nullable-field soup:
// ❌ Don't do this
type FetchResult = {
status: "loading" | "success" | "error";
data?: User;
error?: string;
};
// ✅ Do this
type FetchResult =
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: string };
With the discriminated union, TypeScript knows that when status === "success", data is guaranteed to exist. No optional chaining needed.
Const assertions for literal inference
When you need an array of string literals that TypeScript should treat as the actual values, not as string[]:
const ROLES = ["admin", "student", "mentor", "recruiter", "faculty"] as const;
type Role = (typeof ROLES)[number];
// Type: "admin" | "student" | "mentor" | "recruiter" | "faculty"
This lets you derive your union type from the source of truth rather than maintaining them separately.
Template literal types for API route typing
For typed API endpoints:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type APIRoute = `/api/${string}`;
type Endpoint = `${HTTPMethod} ${APIRoute}`;
// Valid: "GET /api/users"
// Invalid: "PATCH /api/users" (PATCH not in HTTPMethod)
I use this pattern in CodeMyFYP Academy to generate typed route constants that fail at compile time if you construct an invalid endpoint string.
The satisfies operator
Introduced in TypeScript 4.9, satisfies validates that a value conforms to a type while preserving the literal types of the value itself:
const palette = {
red: "#ef4444",
green: "#22c55e",
blue: "#3b82f6",
} satisfies Record<string, string>;
// palette.red is type `"#ef4444"` (literal), not `string`
// But you get an error if you add a non-string value
This is particularly useful for config objects where you want both type safety and autocomplete on literal values.
Branded types for value-object semantics
For domain values that are the same underlying type but should not be mixed:
type UserId = string & { readonly __brand: "UserId" };
type TenantId = string & { readonly __brand: "TenantId" };
function createUserId(id: string): UserId {
return id as UserId;
}
// This fails at compile time:
function getUser(userId: UserId): User { ... }
getUser(tenantId); // Type error!
This catches entire classes of bugs where you accidentally pass the wrong string ID to a function.
The pattern that made TypeScript click for me
Stop trying to annotate everything manually. Let TypeScript infer as much as possible, and only annotate at API boundaries (function parameters, return types of public functions, exported types). The type system is most useful when it catches errors at the call site — and it can only do that if the inference chain is unbroken.
Every time you write as any or as unknown, you're cutting the inference chain. The discipline of refusing to reach for type assertions has, for me, been the single biggest source of TypeScript-caught bugs.