TL;DR
Master advanced TypeScript patterns: discriminated unions, template literal types, conditional types, infer, and branded types.
Key concepts
- TypeScript advanced types
- template literal types
- conditional types TypeScript
- branded types
Advanced Patterns
TypeScript's type system is powerful enough to encode complex business rules. These advanced patterns let you catch entire categories of bugs at compile time.
Discriminated Unions
Use a shared literal property to create type-safe unions with exhaustive handling.
interface Circle { kind: "circle"; radius: number }
interface Rectangle { kind: "rectangle"; width: number; height: number }
interface Triangle { kind: "triangle"; base: number; height: number }
type Shape = Circle | Rectangle | Triangle;
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
}
}
function describe(shape: Shape): string {
switch (shape.kind) {
case "circle": return `Circle with radius ${shape.radius}`;
case "rectangle": return `${shape.width}x${shape.height} rectangle`;
case "triangle": return `Triangle with base ${shape.base}`;
}
}
// Exhaustive — the compiler ensures every case is handled
const shapes: Shape[] = [
{ kind: "circle", radius: 5 },
{ kind: "rectangle", width: 10, height: 3 },
{ kind: "triangle", base: 8, height: 6 }
];
shapes.forEach(shape => {
console.log(`${describe(shape)}: area = ${area(shape).toFixed(2)}`);
});
// State machines with discriminated unions
type RequestState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function render(state: RequestState): string {
switch (state.status) {
case "idle": return "Ready to fetch";
case "loading": return "Loading...";
case "success": return `Got ${state.data.length} items`;
case "error": return `Error: ${state.message}`;
}
}
const states: RequestState[] = [
{ status: "idle" },
{ status: "loading" },
{ status: "success", data: ["a", "b", "c"] },
{ status: "error", message: "Network timeout" }
];
states.forEach(s => console.log(`[${s.status}] ${render(s)}`));
Template Literal Types
Build string types from other types using template literal syntax.
// Basic template literals
type Color = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";
type ClassName = `${Size}-${Color}`;
// ClassName = "sm-red" | "sm-blue" | "sm-green" | "md-red" | ...
const valid: ClassName = "lg-blue";
console.log(`Class: ${valid}`);
// Event handler names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// HandlerName = "onClick" | "onFocus" | "onBlur"
const handlers: Record<HandlerName, () => void> = {
onClick: () => console.log("clicked"),
onFocus: () => console.log("focused"),
onBlur: () => console.log("blurred")
};
Object.entries(handlers).forEach(([name, fn]) => {
console.log(`Calling ${name}:`);
fn();
});
// CSS property builder
type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSDirectional = `${CSSProperty}-${Direction}`;
const styles: Partial<Record<CSSDirectional, string>> = {
"margin-top": "10px",
"padding-left": "20px",
"margin-bottom": "5px"
};
Object.entries(styles).forEach(([prop, val]) => {
console.log(`${prop}: ${val}`);
});
Conditional Types
Types that choose different results based on a condition.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Flatten arrays
type Flatten<T> = T extends Array<infer U> ? U : T;
type C = Flatten<string[]>; // string
type D = Flatten<number>; // number
// Extract promise values
type Awaited<T> = T extends Promise<infer U> ? U : T;
type E = Awaited<Promise<string>>; // string
type F = Awaited<number>; // number
// Practical: type-safe API response handler
interface ApiResponse<T> {
data: T;
status: number;
}
type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;
type UserResponse = ApiResponse<{ name: string; email: string }>;
type UserData = ExtractData<UserResponse>; // { name: string; email: string }
// Runtime demonstration
function processResponse<T>(response: ApiResponse<T>): T {
if (response.status >= 400) {
throw new Error(`API error: ${response.status}`);
}
return response.data;
}
const response: UserResponse = {
data: { name: "Alice", email: "alice@example.com" },
status: 200
};
const user = processResponse(response);
console.log(`User: ${user.name} (${user.email})`);
// Distribute over unions
type ToArray<T> = T extends unknown ? T[] : never;
type G = ToArray<string | number>; // string[] | number[]
const strings: string[] = ["a", "b"];
const numbers: number[] = [1, 2];
const mixed: G = strings; // or numbers — both are valid
console.log(`Mixed: ${mixed}`);
Branded Types
Prevent mixing values that share the same underlying type but have different meanings.
// Without branding, these are interchangeable (bug-prone!)
// type UserId = string;
// type OrderId = string;
// With branding, they're distinct types
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
// Constructor functions validate and brand
function UserId(id: string): UserId {
if (!id.startsWith("usr_")) throw new Error("Invalid user ID");
return id as UserId;
}
function OrderId(id: string): OrderId {
if (!id.startsWith("ord_")) throw new Error("Invalid order ID");
return id as OrderId;
}
function Email(email: string): Email {
if (!email.includes("@")) throw new Error("Invalid email");
return email as Email;
}
// Functions accept only the correct branded type
function getUser(id: UserId): string {
return `User(${id})`;
}
function getOrder(id: OrderId): string {
return `Order(${id})`;
}
function sendEmail(to: Email, subject: string): string {
return `Email to ${to}: ${subject}`;
}
// Usage — types prevent mixing up IDs
const userId = UserId("usr_123");
const orderId = OrderId("ord_456");
const email = Email("alice@example.com");
console.log(getUser(userId));
console.log(getOrder(orderId));
console.log(sendEmail(email, "Welcome!"));
// These would cause compile errors:
// getUser(orderId); // Error: OrderId is not UserId
// getOrder(userId); // Error: UserId is not OrderId
// sendEmail("plain string", "Hi"); // Error: string is not Email
console.log("\nAll branded operations succeeded!");
Mapped Types with Modifiers
Transform types by adding, removing, or changing properties.
// Make all properties readonly
type Immutable<T> = { readonly [K in keyof T]: T[K] };
// Make all properties mutable (remove readonly)
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
// Make all properties required (remove optional)
type Complete<T> = { [K in keyof T]-?: T[K] };
// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Config {
readonly host: string;
readonly port: number;
debug?: boolean;
logLevel?: string;
}
// Remove readonly
type MutableConfig = Mutable<Config>;
// Make everything required
type FullConfig = Complete<Config>;
// Runtime demonstration
const config: FullConfig = {
host: "localhost",
port: 3000,
debug: true,
logLevel: "info"
};
console.log("Full config:", JSON.stringify(config));
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface AppState {
user: { name: string; prefs: { theme: string } };
items: string[];
}
const state: DeepReadonly<AppState> = {
user: { name: "Alice", prefs: { theme: "dark" } },
items: ["a", "b"]
};
// state.user.name = "Bob"; // Error: readonly
console.log(`State: ${state.user.name}, theme: ${state.user.prefs.theme}`);
Try It Yourself
// Build a type-safe event emitter
type EventMap = {
userLogin: { userId: string; timestamp: number };
userLogout: { userId: string };
pageView: { path: string; referrer?: string };
error: { message: string; code: number };
};
class TypedEmitter<T extends Record<string, unknown>> {
private handlers = new Map<keyof T, Array<(payload: any) => void>>();
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
const list = this.handlers.get(event) ?? [];
list.forEach(fn => fn(payload));
}
off<K extends keyof T>(event: K): void {
this.handlers.delete(event);
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("userLogin", ({ userId, timestamp }) => {
console.log(`Login: ${userId} at ${new Date(timestamp).toISOString()}`);
});
emitter.on("pageView", ({ path, referrer }) => {
console.log(`Page: ${path}${referrer ? ` (from ${referrer})` : ""}`);
});
emitter.on("error", ({ message, code }) => {
console.log(`Error ${code}: ${message}`);
});
// Emit events — payload is type-checked!
emitter.emit("userLogin", { userId: "usr_123", timestamp: Date.now() });
emitter.emit("pageView", { path: "/dashboard" });
emitter.emit("pageView", { path: "/settings", referrer: "/dashboard" });
emitter.emit("error", { message: "Not found", code: 404 });
// These would cause compile errors:
// emitter.emit("userLogin", { wrong: "field" });
// emitter.emit("unknown", {});
Key Takeaways
- Discriminated unions with
switchprovide exhaustive, type-safe branching - Template literal types build string types from unions:
`${A}-${B}` - Conditional types (
T extends U ? X : Y) enable type-level logic inferextracts types from inside other types (arrays, promises, functions)- Branded types prevent mixing values with the same underlying type
- Mapped type modifiers (
-readonly,-?) transform property attributes
Pro Tip: Start with discriminated unions — they solve 80% of cases where you'd reach for advanced patterns. Only reach for conditional types and
inferwhen you're building reusable library types. For application code, simple unions with exhaustive switches are almost always clearer and sufficient.
Next Steps
You've learned the individual pieces — now it's time to combine them. In the capstone project, you'll build a complete type-safe task management system using generics, discriminated unions, utility types, and event systems.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.