| name | type-system-design |
| description | Design expressive type systems that catch bugs at compile time. Outputs type hierarchy, branded/nominal types, discriminated unions, generic constraints, and type-safe API patterns. |
| argument-hint | ["language","codebase scale","team TypeScript experience","key domain concepts"] |
| allowed-tools | Read, Write |
Type System Design
A well-designed type system catches entire categories of bugs before code runs. The goal is not maximal type coverage — it's using types to make illegal states unrepresentable and to guide developers toward correct usage.
Core Patterns
Branded/Nominal Types
declare const brand: unique symbol;
type Brand<T, B> = T & { [brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type ProductId = Brand<string, "ProductId">;
const toUserId = (id: string): UserId => id as UserId;
const toOrderId = (id: string): OrderId => id as OrderId;
const toProductId = (id: string): ProductId => id as ProductId;
function getOrder(orderId: OrderId): Order { ... }
const userId = toUserId("usr-123");
getOrder(userId);
Discriminated Unions (Make Illegal States Unrepresentable)
interface Order {
status: "draft" | "paid" | "shipped";
paymentId?: string;
trackingNumber?: string;
}
type Order =
| { status: "draft"; items: OrderItem[] }
| { status: "paid"; items: OrderItem[]; paymentId: string }
| { status: "shipped"; items: OrderItem[]; paymentId: string; trackingNumber: string };
function getStatusLabel(order: Order): string {
switch (order.status) {
case "draft": return "Pending";
case "paid": return "Paid";
case : order.;
}
}
Generic Constraints
type Repository<T extends { id: string }> = {
findById(id: string): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: string): Promise<void>;
};
type Result<T, E extends Error = Error> =
| { success: true; value: T }
| { success: false; error: E };
class ValidationError extends Error { constructor(public field: string, message: string) { super(message); } }
class NotFoundError extends Error {}
async function findOrder(id: OrderId): <<, >> {
order = db..(id);
(!order) { : , : () };
{ : , : order };
}
result = (());
(result.) {
.(result..);
} {
.(result..);
}
Template Literal Types
type EntityType = "order" | "user" | "product";
type EventAction = "created" | "updated" | "deleted";
type EventName = `${EntityType}.${EventAction}`;
type EventMap = {
[K in EventName]: K extends `${infer E}.${infer A}`
? { entity: E; action: A; timestamp: string }
: never;
};
function emit<K extends EventName>(event: K, payload: EventMap[K]): void { ... }
emit("order.created", { entity: "order", action: "created", timestamp: "..." });
emit("cart.created", { ... });
Readonly and Immutability
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type ImmutableOrder = DeepReadonly<Order>;
function processOrder(order: Readonly<Order>): ProcessedOrder {
return { ...order, processedAt: new Date() };
}
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
any everywhere | Defeats type checking entirely | Use unknown + type guards; narrow incrementally |
| Optional fields for state | Invalid combinations compile | Discriminated unions per state |
| String IDs without branding | Wrong ID type passed silently | Branded types per entity |
Type assertions (as) | Bypasses type safety | Use type guards with runtime checks |
| Overly wide types | string when "draft" | "paid" is correct | Narrow types at boundaries |
10 Rules
- Make illegal states unrepresentable — use discriminated unions, not optional fields.
- Brand primitive types (string IDs, amounts) to prevent mixing.
unknown instead of any — forces explicit narrowing.
- Exhaustive switch statements on discriminated unions — catch missing cases at compile time.
- Result types for operations that can fail — no unchecked exceptions.
Readonly<T> for function parameters that must not be mutated.
- Generic constraints express requirements — don't accept
any when { id: string } is sufficient.
- Type aliases document intent —
UserId is more readable than string.
- Utility types (Pick, Omit, Partial) reuse and transform types — don't duplicate.
- Types are documentation — readable types reduce the need for comments.