EngineeringJune 2, 20265 min read
After shipping 30+ TypeScript projects across healthcare, fintech, and SaaS, these are the patterns we enforce from day one. They prevent entire categories of bugs.
1. Branded Types
Plain strings and numbers can be accidentally swapped. Branded types make the type system prevent this at compile time.
// Without branded types — this compiles but is wrong
function chargeUser(userId: string, amount: number) { ... }
chargeUser(orderId, userId); // TS doesn't catch this!
// With branded types
type UserId = string & { readonly _brand: "UserId" };
type OrderId = string & { readonly _brand: "OrderId" };
type USD = number & { readonly _brand: "USD" };
function createUserId(id: string): UserId { return id as UserId; }
function createOrderId(id: string): OrderId { return id as OrderId; }
function chargeUser(userId: UserId, amount: USD) { ... }
// Now this is a compile error!
chargeUser(orderId, userId); // Error: Type 'OrderId' is not assignable to 'UserId'2. Discriminated Unions for API States
// Bad — checking .data when in error state compiles fine
type ApiState = {
loading: boolean;
data?: User;
error?: string;
};
// Good — exhaustive type checking
type ApiState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function UserProfile({ state }: { state: ApiState<User> }) {
switch (state.status) {
case "loading": return <Spinner />;
case "error": return <Error message={state.error} />;
case "success": return <Profile user={state.data} />;
// TypeScript warns if you forget a case!
}
}3. Zod for Runtime Validation
TypeScript types disappear at runtime. Use Zod to validate at the API boundary — anything coming from external sources (user input, API responses, env vars).
import { z } from "zod";
const CreateOrderSchema = z.object({
userId: z.string().uuid(),
items: z.array(z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(100),
})).min(1),
currency: z.enum(["USD", "EUR", "NPR"]),
});
type CreateOrderInput = z.infer<typeof CreateOrderSchema>;
// In your API handler
export async function POST(req: Request) {
const body = await req.json();
const result = CreateOrderSchema.safeParse(body);
if (!result.success) {
return Response.json({ errors: result.error.flatten() }, { status: 400 });
}
// result.data is fully typed and validated
const order = await createOrder(result.data);
return Response.json(order);
}Define your Zod schemas first, then derive TypeScript types from them with z.infer<>. This ensures runtime and compile-time validation always stay in sync.
TypeScriptPatternsBest Practices

