thecodex.expert · The Codex Family of Knowledge
Snippets

TypeScript Snippets

13 idiomatic TypeScript patterns — copy, paste, adapt.

TypeScript 5.x 13 snippets strict: true
TypeScriptgenerics.ts
#9654; Try it
Generic functions and constraints
// Generic function: T is constrained to types with a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

console.log(longest("hello", "hi"));        // "hello"
console.log(longest([1, 2, 3], [4, 5]));    // [1, 2, 3]
// longest(10, 20);  // Error: number has no length

// Generic with default
function createPair<T = string, U = number>(a: T, b: U): [T, U] {
  return [a, b];
}

const p1 = createPair("hello", 42);         // [string, number]
const p2 = createPair<boolean, Date>(true, new Date());

// Constrained key lookup
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, active: true };
const name = getProperty(user, "name");  // type: string
const age  = getProperty(user, "age");   // type: number
// getProperty(user, "missing");  // Error: not a key of typeof user
TypeScriptutility_types.ts
#9654; Try it
Built-in utility types
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// Partial: all properties optional — for partial updates
type UserUpdate = Partial<User>;

// Required: all properties required — reverse of Partial
type UserRequired = Required<User>;

// Pick: select specific properties
type PublicUser = Pick<User, "id" | "name" | "email">;

// Omit: exclude specific properties
type UserWithoutPassword = Omit<User, "password">;

// Readonly: prevent mutation
const frozenUser: Readonly<User> = {
  id: 1, name: "Alice", email: "a@b.c",
  password: "hash", createdAt: new Date()
};
// frozenUser.name = "Bob";  // Error: readonly

// Record: key-value map type
type RolePermissions = Record<"admin" | "user" | "guest", string[]>;
const permissions: RolePermissions = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"],
};

// ReturnType, Parameters
function fetchUser(id: number, format: "json" | "xml"): Promise<User> {
  return Promise.resolve({} as User);
}
type FetchReturn = ReturnType<typeof fetchUser>;       // Promise<User>
type FetchParams = Parameters<typeof fetchUser>;       // [number, "json"|"xml"]
TypeScriptdiscriminated_unions.ts
#9654; Try it
Discriminated unions for typed state
// Discriminated union: common "kind" field narrows the type
type NetworkState =
  | { kind: "idle" }
  | { kind: "loading"; requestId: string }
  | { kind: "success"; data: unknown; timestamp: Date }
  | { kind: "error"; error: Error; retryCount: number };

function handleState(state: NetworkState): string {
  switch (state.kind) {
    case "idle":    return "Ready";
    case "loading": return `Loading (${state.requestId})`;
    case "success": return `Done at ${state.timestamp.toISOString()}`;
    case "error":   return `Failed: ${state.error.message} (${state.retryCount} retries)`;
    // TypeScript warns if any case is missing — exhaustiveness check
  }
}

// Exhaustiveness helper: errors at compile time if a case is missed
function assertNever(x: never): never {
  throw new Error("Unexpected value: " + x);
}

// Result type pattern
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return { ok: false, error: "Division by zero" };
  return { ok: true, value: a / b };
}

const r = divide(10, 2);
if (r.ok) console.log(r.value);   // 5 — type narrowed to {ok:true, value:number}
else      console.log(r.error);   // narrowed to {ok:false, error:string}
TypeScripttype_guards.ts
#9654; Try it
Type guards and narrowing
// User-defined type guard: predicate x is T
function isString(x: unknown): x is string {
  return typeof x === "string";
}

function isUser(x: unknown): x is { name: string; age: number } {
  return (
    typeof x === "object" && x !== null &&
    "name" in x && typeof (x as any).name === "string" &&
    "age" in x && typeof (x as any).age === "number"
  );
}

// Narrowing with in operator
type Dog = { breed: string; bark(): void };
type Cat = { indoor: boolean; meow(): void };
type Pet = Dog | Cat;

function speak(pet: Pet) {
  if ("bark" in pet) {
    pet.bark();  // TypeScript knows it's Dog here
  } else {
    pet.meow();  // TypeScript knows it's Cat here
  }
}

// instanceof narrowing
function formatError(e: unknown): string {
  if (e instanceof Error) return e.message;          // Error
  if (typeof e === "string") return e;               // string
  if (typeof e === "object" && e !== null) return JSON.stringify(e);
  return String(e);
}

// Assertion functions (TypeScript 3.7+)
function assert(condition: unknown, msg: string): asserts condition {
  if (!condition) throw new Error(msg);
}

function processUser(user: User | null) {
  assert(user !== null, "User must not be null");
  console.log(user.name);  // user is narrowed to User here
}
TypeScriptmapped_types.ts
#9654; Try it
Mapped types and template literal types
// Mapped type: transform each property in a type
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Optional<T> = { [K in keyof T]?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };  // remove readonly

// Conditional mapped type: change value type based on condition
type Serialize<T> = {
  [K in keyof T]: T[K] extends Date ? string : T[K];
};

interface Event {
  id: number;
  name: string;
  startDate: Date;
  endDate: Date;
}

type SerializedEvent = Serialize<Event>;
// { id: number; name: string; startDate: string; endDate: string }

// Template literal types (TypeScript 4.1+)
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// Build getter/setter pairs
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (v: T[K]) => void;
};

type UserAccessors = Getters<Pick<User, "name" | "email">> &
                     Setters<Pick<User, "name" | "email">>;
// { getName: () => string; getEmail: () => string;
//   setName: (v:string)=>void; setEmail: (v:string)=>void }
TypeScriptconditional_types.ts
#9654; Try it
Conditional types and infer
// Conditional type: T extends U ? X : Y
type IsArray<T> = T extends any[] ? true : false;
type IsString<T> = T extends string ? true : false;

type A = IsArray<number[]>;  // true
type B = IsArray<string>;    // false

// infer: extract a type from within another type
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type UnpackPromise<T> = T extends Promise<infer R> ? R : T;
type UnpackFunction<T> = T extends (...args: any[]) => infer R ? R : never;

type C = UnpackArray<string[]>;          // string
type D = UnpackPromise<Promise<User>>;   // User
type E = UnpackFunction<() => number>;   // number

// Distributive conditional types: apply to each union member
type NonNullable<T> = T extends null | undefined ? never : T;
type Clean = NonNullable<string | null | undefined | number>;  // string | number

// Deep readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Config {
  database: { host: string; port: number };
  features: string[];
}
const cfg: DeepReadonly<Config> = {
  database: { host: "localhost", port: 5432 },
  features: ["auth", "logging"],
};
// cfg.database.port = 3000;  // Error: readonly
TypeScriptinterfaces_vs_types.ts
#9654; Try it
Interface vs type alias — when to use each
// Interface: extensible — supports declaration merging
interface Animal {
  name: string;
  eat(): void;
}
interface Animal {          // merges with first declaration — useful for augmenting libs
  sleep(): void;
}
interface Dog extends Animal {
  breed: string;
  bark(): void;
}

// Type alias: more powerful — supports unions, intersections, primitives
type ID = string | number;
type Callback = (err: Error | null, result?: unknown) => void;
type StringOrArray = string | string[];

// Intersection: combine multiple types
type AdminUser = User & { permissions: string[]; superAdmin: boolean };

// Rules of thumb:
// Use interface for: objects, classes, public API shapes, extensible definitions
// Use type for: unions, intersections, primitives, computed types, tuples

// Tuple types: fixed-length arrays with typed positions
type Pair<T, U> = [T, U];
type RGB = [r: number, g: number, b: number];  // labelled tuple

const color: RGB = [255, 128, 0];
const [r, g, b] = color;

// Rest in tuples (variadic tuple types)
type StringsAndNumber = [...string[], number];
const s: StringsAndNumber = ["a", "b", 42];
TypeScriptclasses_decorators.ts
#9654; Try it
Classes with access modifiers and decorators
// TypeScript access modifiers
class BankAccount {
  readonly id: string;
  private _balance: number;
  protected owner: string;  // accessible in subclasses

  constructor(owner: string, initialBalance: number) {
    this.id = crypto.randomUUID();
    this.owner = owner;
    this._balance = initialBalance;
  }

  // Parameter shorthand: public/private in constructor
  // constructor(public readonly id: string, private _balance: number) {}

  get balance(): number { return this._balance; }

  deposit(amount: number): this {  // return this for method chaining
    if (amount <= 0) throw new RangeError("Amount must be positive");
    this._balance += amount;
    return this;
  }
}

// Abstract class: cannot be instantiated directly
abstract class Shape {
  abstract area(): number;
  abstract perimeter(): number;
  describe(): string { return `Area: ${this.area().toFixed(2)}`; }
}

class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area(): number { return Math.PI * this.radius ** 2; }
  perimeter(): number { return 2 * Math.PI * this.radius; }
}

// Class implements interface
interface Serializable {
  toJSON(): object;
  fromJSON(data: object): void;
}

class Config implements Serializable {
  constructor(private data: Record<string, unknown> = {}) {}
  toJSON() { return { ...this.data }; }
  fromJSON(d: object) { Object.assign(this.data, d); }
}
TypeScriptbranded_types.ts
#9654; Try it
Branded types for type-safe IDs
// Branded type: wrap a primitive with a phantom type tag
// Prevents accidentally mixing UserId with OrderId even though both are strings
type Brand<T, B> = T & { readonly _brand: B };

type UserId  = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email   = Brand<string, "Email">;

// Constructors / smart constructors validate and brand
function UserId(id: string): UserId {
  if (!id.startsWith("usr_")) throw new Error("Invalid UserId");
  return id as UserId;
}

function Email(email: string): Email {
  if (!email.includes("@")) throw new Error("Invalid email");
  return email as Email;
}

function getUser(id: UserId): void {
  console.log("Fetching user:", id);
}

const uid = UserId("usr_123");
const oid = "ord_456" as OrderId;

getUser(uid);   // OK
// getUser(oid);   // Error: OrderId is not UserId
// getUser("usr_789");  // Error: string is not UserId

// Nominal pattern for validated values
type PositiveNumber = Brand<number, "PositiveNumber">;
function toPositive(n: number): PositiveNumber {
  if (n <= 0) throw new RangeError("Must be positive");
  return n as PositiveNumber;
}
TypeScriptasync_types.ts
#9654; Try it
Async patterns with typed errors
// Typed async result — avoids thrown exceptions crossing async boundaries
type AsyncResult<T, E = Error> = Promise<
  | { success: true; data: T }
  | { success: false; error: E }
>;

async function safeFetch<T>(url: string): AsyncResult<T> {
  try {
    const res = await fetch(url);
    if (!res.ok) {
      return { success: false, error: new Error(`HTTP ${res.status}`) };
    }
    const data = await res.json() as T;
    return { success: true, data };
  } catch (err) {
    return { success: false, error: err instanceof Error ? err : new Error(String(err)) };
  }
}

interface User { id: number; name: string }

async function loadUser(id: number) {
  const result = await safeFetch<User>(`/api/users/${id}`);
  if (result.success) {
    console.log(result.data.name);   // data is User — fully typed
  } else {
    console.error(result.error.message);
  }
}

// Awaited: unwrap Promise type
type Resolved = Awaited<Promise<User>>;  // User
type Deep     = Awaited<Promise<Promise<string>>>;  // string
TypeScriptdeclaration_merging.ts
#9654; Try it
Module augmentation and declaration merging
// Extend an existing library's types without modifying source
// Example: add custom property to Express Request
declare global {
  namespace Express {
    interface Request {
      user?: { id: string; roles: string[] };
      requestId: string;
    }
  }
}

// Augment a module
declare module "some-library" {
  interface Config {
    customOption?: boolean;
  }
}

// Extend built-in types
interface Array<T> {
  first(): T | undefined;
  last(): T | undefined;
}

Array.prototype.first = function() { return this[0]; };
Array.prototype.last  = function() { return this[this.length - 1]; };

// const type assertion: narrow literal types
const config = {
  endpoint: "https://api.example.com",
  method: "GET",
  retries: 3,
} as const;
// config.method is "GET" (literal), not string
// config.retries is 3 (literal), not number
// All properties are readonly

type ConfigMethod = typeof config.method;  // "GET"
TypeScriptsatisfies.ts
#9654; Try it
satisfies operator (TypeScript 4.9)
// satisfies: validate type without widening — keeps literal types
type Color = "red" | "green" | "blue" | `#${string}`;
type Palette = Record<string, Color | Color[]>;

// Without satisfies: type is widened to Palette — can't use string methods
const palette1: Palette = {
  red: "#ff0000",
  green: "green",
  multicolor: ["red", "blue"],
};
// palette1.red.toUpperCase();  // Error: Color might be Color[]

// With satisfies: validates against Palette but keeps precise types
const palette2 = {
  red: "#ff0000",      // type: "#ff0000" (literal), checked against Color
  green: "green",      // type: "green"
  multicolor: ["red", "blue"],  // type: string[]
} satisfies Palette;

palette2.red.toUpperCase();            // OK — TypeScript knows it's string
palette2.multicolor.map(c => c);      // OK — TypeScript knows it's array

// Another use: const with type checking
const routes = {
  "/": { component: "Home", auth: false },
  "/dashboard": { component: "Dashboard", auth: true },
} satisfies Record<string, { component: string; auth: boolean }>;

// routes["/"].component is string (not widened to unknown)
TypeScriptzod_pattern.ts
#9654; Try it
Runtime validation pattern (without Zod dependency)
// TypeScript types are erased at runtime — validate incoming data explicitly

// Simple validator type
type Validator<T> = (data: unknown) => { valid: true; value: T } | { valid: false; error: string };

function validateUser(data: unknown): { valid: true; value: User } | { valid: false; error: string } {
  if (typeof data !== "object" || data === null) {
    return { valid: false, error: "Expected object" };
  }
  const obj = data as Record<string, unknown>;
  if (typeof obj.id !== "number")   return { valid: false, error: "id must be number" };
  if (typeof obj.name !== "string") return { valid: false, error: "name must be string" };
  if (typeof obj.email !== "string") return { valid: false, error: "email must be string" };
  return { valid: true, value: { id: obj.id, name: obj.name, email: obj.email, password: "", createdAt: new Date() } };
}

// Parse API response safely
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  const raw = await res.json();
  const result = validateUser(raw);
  if (!result.valid) throw new Error(`Invalid user data: ${result.error}`);
  return result.value;  // type: User — safe to use
}

// In practice: use Zod, Valibot, or Arktype for production validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(rawData);  // throws on invalid
TypeScript referenceTS overview
← Java Go snippets →
Everything TypeScript in one place — learning paths, reference, playground, and more. TypeScript Hub →