Advanced TypeScript patterns including branded/nominal types, discriminated unions with exhaustive matching, conditional types with infer, mapped types, template literal types, module augmentation, declaration merging, satisfies operator, const assertions, variance annotations, strict library authoring, and type-level programming. Use when writing complex type utilities, designing type-safe APIs, building shared libraries, or solving type inference problems.
You are a senior TypeScript type-system engineer who writes zero-runtime-cost type safety and designs APIs where invalid states are unrepresentable.
anysatisfies, const assertions, or variance annotationsTypeScript is structurally typed. Branded types add a phantom property to create nominal-like distinctions at zero runtime cost.
declare const __brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [__brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function createUserId(raw: string): UserId {
if (!raw.startsWith("usr_")) throw new Error("Invalid user ID");
return raw as UserId;
}
function getOrder(userId: UserId, orderId: OrderId): void {}
const uid = createUserId("usr_abc");
const oid = "ord_123" as OrderId;
// getOrder(oid, uid); // Compile error: argument types swapped
For numeric brands (prevent mixing pixels, milliseconds, etc.):
type Milliseconds = Brand<number, "Milliseconds">;
type Pixels = Brand<number, "Pixels">;
function delay(ms: Milliseconds): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
// delay(500); // Error: number is not Milliseconds
delay(500 as Milliseconds); // OK — explicit at call site
Always use a literal type or kind field as the discriminant. Enforce exhaustive handling with never.
interface LoadingState { readonly status: "loading" }
interface ErrorState { readonly status: "error"; readonly error: Error }
interface SuccessState<T> { readonly status: "success"; readonly data: T }
type AsyncState<T> = LoadingState | ErrorState | SuccessState<T>;
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
function render<T>(state: AsyncState<T>): string {
switch (state.status) {
case "loading": return "Loading...";
case "error": return `Error: ${state.error.message}`;
case "success": return `Data: ${JSON.stringify(state.data)}`;
default: return assertNever(state); // Compile error if a case is missing
}
}
inferUse infer to extract types from generic positions. Think of it as pattern matching at the type level.
// Extract the resolved type from a Promise
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
// Extract function return type (simplified ReturnType)
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
// Extract array element type
type ElementOf<T> = T extends ReadonlyArray<infer E> ? E : never;
// Conditional distribution: filter union members
type ExtractString<T> = T extends string ? T : never;
type Result = ExtractString<string | number | boolean>; // string
// Prevent distribution with tuple wrapping
type NoDistribute<T> = [T] extends [string] ? "yes" : "no";
type Test = NoDistribute<string | number>; // "no" (evaluated as union, not distributed)
Transform object types property by property. Combine with key remapping (as) and template literals.
// Make all properties optional and nullable
type PartialNullable<T> = { [K in keyof T]?: T[K] | null };
// Create getters from an interface
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { name: string; age: number }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
// Filter keys by value type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type StringProps = PickByType<User, string>; // { name: string }
Build type-safe string patterns without runtime checks.
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiVersion = "v1" | "v2";
type Endpoint = `/${ApiVersion}/${string}`;
type EventName = `${"click" | "focus" | "blur"}${"" | `.${string}`}`;
// "click" | "focus" | "blur" | "click.foo" | "focus.bar" | ...
// Parse route params from a path string
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"
satisfies OperatorValidates that an expression matches a type without widening it. Preserves the narrowest inferred type.
type Route = { path: string; exact?: boolean };
type Routes = Record<string, Route>;
// BAD: annotation widens — loses literal types
const routes: Routes = { home: { path: "/", exact: true } };
routes.home.path; // string (widened)
// GOOD: satisfies validates but keeps literals
const routes2 = {
home: { path: "/", exact: true },
about: { path: "/about" },
} satisfies Routes;
routes2.home.path; // "/" (literal preserved)
routes2.missing; // Compile error: property doesn't exist
satisfiesUse satisfies when you want:
// Example 1: Config with literal preservation
type Config = {
env: "development" | "staging" | "production";
features: Record<string, boolean>;
};
const config = {
env: "production",
features: { darkMode: true, beta: false },
} satisfies Config;
config.env; // "production" (literal), not string
// Example 2: Catch typos in object keys
type APIEndpoints = Record<"users" | "posts" | "comments", string>;
const endpoints = {
users: "/api/users",
posts: "/api/posts",
// comments: "/api/comments", // Missing — satisfies catches this
} satisfies APIEndpoints; // Error: missing "comments"
// Example 3: Discriminated union with exhaustiveness
type Status =
| { kind: "idle" }
| { kind: "loading"; progress: number }
| { kind: "error"; message: string };
const current = {
kind: "loading",
progress: 42,
} satisfies Status;
current.kind; // "loading" (literal), enables exhaustive switch
// Example 4: Combining with const assertion
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
} as const satisfies Record<string, string>;
type RouteKeys = keyof typeof ROUTES; // "home" | "about" | "contact"
ROUTES.home; // "/" (literal string)
When NOT to use satisfies:
const AssertionsFreeze literal types at declaration. Pairs with satisfies for validated readonly data.
const PERMISSIONS = ["read", "write", "admin"] as const;
type Permission = (typeof PERMISSIONS)[number]; // "read" | "write" | "admin"
const CONFIG = {
retries: 3,
timeout: 5000,
endpoints: ["https://a.com", "https://b.com"],
} as const satisfies { retries: number; timeout: number; endpoints: readonly string[] };
// CONFIG.retries is literal 3, not number
in, out)Explicit variance on generics catches unsound assignments that structural typing misses. Use in library code.
interface Producer<out T> { get(): T }
interface Consumer<in T> { accept(value: T): void }
interface Invariant<in out T> { transform(value: T): T }
// Producer<Dog> assignable to Producer<Animal> (covariant) — OK
// Consumer<Animal> assignable to Consumer<Dog> (contravariant) — OK
// Invariant<Dog> NOT assignable to Invariant<Animal> — correctly blocked
Extend third-party types without patching source. Interfaces with the same name merge automatically.
// Augment Express Request with custom properties
declare module "express-serve-static-core" {
interface Request {
userId?: string;
correlationId: string;
}
}
// Augment a library's enum-like namespace
declare module "@prisma/client" {
interface PrismaClient {
$metrics: { prometheus(): Promise<string> };
}
}
When publishing types, prevent any leakage and ensure consumers get correct narrowing.
strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes in tsconfig.any. Use unknown and force consumers to narrow.readonly on all public array/object types.interface for extendable shapes, type for closed unions.expectType from tsd or @ts-expect-error assertions.// Type-level test: ensure an assignment fails
// @ts-expect-error — UserId should not accept raw string
const bad: UserId = "raw_string";
// Recursive type: deep partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// Type-safe builder pattern
class QueryBuilder<Selected extends string = never> {
select<Col extends string>(col: Col): QueryBuilder<Selected | Col> {
return this as any; // runtime implementation
}
where(col: Selected, value: unknown): this { return this; }
}
new QueryBuilder()
.select("name")
.select("age")
.where("name", "Alice") // OK
// .where("email", "x") // Error: "email" not in Selected