Advanced TypeScript: Generics, Conditional Types, Template Literals & Branded Types
A masterclass in modern TypeScript type engineering: Discriminated unions with exhaustive checking, conditional types with infer, template literal types, the satisfies operator, and zero-runtime branded types.
1. Discriminated Unions & Exhaustive Type Checking
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function renderState<T>(state: AsyncState<T>): string {
switch (state.status) {
case "idle":
return "Ready to load";
case "loading":
return "Fetching data...";
case "success":
return `Loaded: ${JSON.stringify(state.data)}`;
case "error":
return `Failed: ${state.error.message}`;
default: {
// Compile error if any status variant was omitted above!
const _unreachable: never = state;
throw new Error(`Unhandled state: ${_unreachable}`);
}
}
}If you add a new status like "revalidating", TypeScript flags an immediate compile error at _unreachable before running the code.
| Pattern | Implementation | Advantage | Copy |
|---|---|---|---|
| Optional Properties | { data?: T; error?: Error } | Permits illegal states (e.g. data AND error simultaneously) | |
| Discriminated Union | { status: "success"; data: T } | ... | Makes invalid states unrepresentable in the type system | |
| Exhaustive Check | const _exhaustive: never = val; | Zero runtime overhead; catches unhandled branches at build time |
2. Conditional Types & Pattern Matching with infer
// 1. Extract the inner resolved type of any Promise or nested Promise
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
// 2. Extract the element type from an array or readonly array
type ElementOf<T> = T extends (infer E)[] ? E : T extends readonly (infer E)[] ? E : never;
// 3. Extract the exact return type of an asynchronous function
type AsyncReturnType<T extends (...args: any) => Promise<any>> =
T extends (...args: any) => Promise<infer R> ? R : never;
// Example usage:
async function fetchUserProfile(id: string) {
return { id, name: "Alice", roles: ["admin", "editor"] as const };
}
type Profile = AsyncReturnType<typeof fetchUserProfile>;
// Result: { id: string; name: string; roles: readonly ["admin", "editor"] }infer acts like a wildcard pattern variable inside conditional expressions.
3. Mapped Types, Key Remapping & Template Literals
type Entity = {
user: { id: string; email: string };
order: { orderId: number; total: number };
product: { sku: string; price: number };
};
// Automatically generate strictly typed event listeners: "onUserChange", "onOrderChange"
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (entity: T[K]) => void;
};
type AppEvents = EventHandlers<Entity>;
/* Resolves automatically to:
{
onUserChange: (entity: { id: string; email: string }) => void;
onOrderChange: (entity: { orderId: number; total: number }) => void;
onProductChange: (entity: { sku: string; price: number }) => void;
}
*/Key remapping with template literals eliminates boilerplate string dictionaries and keeps API handlers tightly synchronized.
4. The satisfies Operator vs Type Assertions (as)
type RouteConfig = Record<string, { path: string; secure: boolean }>;
// ANTI-PATTERN: Type annotation widens keys to generic string
const badRoutes: RouteConfig = {
home: { path: "/", secure: false },
dashboard: { path: "/dashboard", secure: true }
};
// badRoutes.unknownRoute.path -> No compile error! Returns undefined at runtime!
// PRODUCTION PATTERN: Validates contract while preserving exact keys
const goodRoutes = {
home: { path: "/", secure: false },
dashboard: { path: "/dashboard", secure: true }
} satisfies RouteConfig;
// goodRoutes.home.path -> Autocompleted and verified!
// goodRoutes.settings -> Compile Error: Property "settings" does not exist!Always prefer "satisfies" over "as Type" or widened type annotations on object literals.
5. Branded Types: Zero-Cost Nominal Typing
// Declare unique type brand symbols
declare const BrandSymbol: unique symbol;
type Brand<T, B> = T & { readonly [BrandSymbol]: B };
// Define branded primitive types
export type UserId = Brand<string, "UserId">;
export type OrderId = Brand<string, "OrderId">;
// Smart constructor helpers with runtime validation
function createUserId(id: string): UserId {
if (!id.startsWith("usr_")) throw new Error("Invalid User ID format");
return id as UserId;
}
function chargeUser(userId: UserId, orderId: OrderId) {
console.log(`Charging user ${userId} for order ${orderId}`);
}
const user = createUserId("usr_1024");
const order = "ord_9999" as OrderId;
chargeUser(user, order); // Compiles cleanly!
// chargeUser(order, user); // Compile Error: Type "OrderId" is not assignable to "UserId"!Branded types compile away to plain strings or numbers in JavaScript, introducing zero bytes of runtime overhead.
Related Free Online Tools
Put these techniques into practice immediately with our privacy-first browser utilities:
Convert JSON payloads into strongly-typed TypeScript interfaces.
Format and validate JSON trees with error highlighting.
Minify and optimize compiled JavaScript bundles client-side.