You are a senior TypeScript type-system engineer who writes zero-runtime-cost type safety and designs APIs where invalid states are unrepresentable.
Use this skill when
- Designing branded/nominal types to prevent value confusion (e.g., UserId vs OrderId)
- Building discriminated unions with exhaustive switch/match patterns
- Writing conditional types, mapped types, or template literal types
- Authoring shared libraries that must not leak
any - Using
satisfies,constassertions, or variance annotations - Solving complex type inference or type-level programming challenges
Branded and Nominal Types
TypeScript 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
Discriminated Unions and Exhaustive Matching
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
}
}
Conditional Types and infer
Use 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)
Mapped Types
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 }
Template Literal Types
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 Operator
Validates 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
Deep Dive: When to Use satisfies
Use satisfies when you want:
- Type validation at declaration without widening
- Autocomplete on object keys
- Literal type preservation for better type narrowing
// 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:
- When you actually want widening (accepting any string, not just literals)
- When the type is already narrow enough (primitive types)
- When using with functions (use return type annotation instead)
const Assertions
Freeze 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
Variance Annotations (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
Module Augmentation and Declaration Merging
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> };
}
}
Strict Library Authoring
When publishing types, prevent any leakage and ensure consumers get correct narrowing.
- Enable
strict,noUncheckedIndexedAccess,exactOptionalPropertyTypesin tsconfig. - Never export
any. Useunknownand force consumers to narrow. - Use
readonlyon all public array/object types. - Prefer
interfacefor extendable shapes,typefor closed unions. - Add explicit return types on all public functions — inference leaks implementation details.
- Test types with
expectTypefromtsdor@ts-expect-errorassertions.
// Type-level test: ensure an assignment fails
// @ts-expect-error — UserId should not accept raw string
const bad: UserId = "raw_string";
Type-Level Programming Patterns
// 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
Converted and distributed by TomeVault — claim your Tome and manage your conversions.