← Tech Guides
01

Quick Reference

Essential Types

Type Example Notes
string let name: string = "Ada" Template literals supported
number let age: number = 30 No int/float distinction
boolean let ok: boolean = true
bigint let big: bigint = 9007199254740991n Arbitrary precision integers
null / undefined let x: null = null Use strictNullChecks
symbol let id = Symbol("id") Unique identifiers
any let x: any = "whatever" Escape hatch — avoid in production
unknown let x: unknown = getData() Type-safe alternative to any
void function log(): void {} Functions that return nothing
never function fail(): never { throw ... } Unreachable code / exhaustive checks
object let o: object = { a: 1 } Non-primitive — prefer specific types
T[] / Array<T> let nums: number[] = [1, 2, 3] Both forms are identical
[T, U] let pair: [string, number] Fixed-length typed arrays

Common tsconfig Flags

Flag Value Purpose
strict true Enables all strict type-checking options
target "ES2022" JS version to compile down to
module "NodeNext" Module system for output
moduleResolution "NodeNext" How imports are resolved
esModuleInterop true Fixes CJS/ESM default import compat
skipLibCheck true Skip type-checking .d.ts files for speed
outDir "./dist" Output directory for compiled JS
rootDir "./src" Root directory of source files
declaration true Emit .d.ts type declaration files
resolveJsonModule true Allow importing .json files
noUncheckedIndexedAccess true Add undefined to index signatures

Utility Types

Utility What It Does Example
Partial<T> All properties become optional Partial<User> — for update payloads
Required<T> All properties become required Required<Config> — enforce all fields
Pick<T, K> Select subset of properties Pick<User, "id" | "name">
Omit<T, K> Remove properties Omit<User, "password">
Record<K, V> Map keys to values Record<string, number>
Readonly<T> All properties become readonly Readonly<Config>
Exclude<T, U> Remove types from union Exclude<"a" | "b" | "c", "a"> = "b" | "c"
Extract<T, U> Keep matching types from union Extract<string | number, string> = string
NonNullable<T> Remove null and undefined NonNullable<string | null> = string
ReturnType<T> Extract function return type ReturnType<typeof fetch>
Awaited<T> Unwrap Promise type Awaited<Promise<string>> = string

Essential CLI Commands

tsc --noEmit
Type-check without generating output files. Perfect for CI pipelines.
tsc --watch
Watch mode — recompiles on file changes.
tsc --init
Generate a starter tsconfig.json with commented options.
npx tsx script.ts
Run TypeScript directly without compilation step.
tsc --showConfig
Print the resolved tsconfig after extends/defaults are applied.
tsc --listFiles
Show all files included in compilation — debug unexpected includes.

Package Manager Commands

npm install <pkg>
Install a dependency. Add -D for dev dependencies.
npm install @types/<pkg> -D
Install type definitions for a JS library.
npx tsc --noEmit
Run the project-local TypeScript compiler.
npm run build
Execute the build script defined in package.json.
02

Setup & Tooling

Node.js & Version Management

Install Node.js via nvm (Node Version Manager) to easily switch between versions per project.

# Install nvm (Linux/macOS)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# Install the latest LTS
nvm install --lts
nvm use --lts

# Pin a version for your project
node --version > .nvmrc
nvm use    # reads .nvmrc automatically

# Check installed versions
nvm ls
Tip: On Windows, use nvm-windows or fnm (Fast Node Manager) which is cross-platform and written in Rust.

Package Managers

Node ships with npm, but alternatives offer different tradeoffs.

Manager Install Strengths
npm Ships with Node Universal, no setup. Workspaces built-in.
pnpm npm i -g pnpm Content-addressable store, saves disk. Strict by default.
yarn corepack enable Plug'n'Play, zero-installs, offline cache.
bun curl -fsSL https://bun.sh/install | bash All-in-one runtime + bundler + package manager. Very fast.

tsconfig.json Explained

The TypeScript configuration file controls compilation behavior. Start strict and relax only when needed.

{
  "compilerOptions": {
    // Type checking
    "strict": true,                    // Enable all strict checks
    "noUncheckedIndexedAccess": true,  // arr[0] is T | undefined

    // Module system
    "target": "ES2022",                // Output JS version
    "module": "NodeNext",              // Module format
    "moduleResolution": "NodeNext",    // How to find modules

    // Interop
    "esModuleInterop": true,           // import express from "express"
    "resolveJsonModule": true,         // import data from "./data.json"
    "isolatedModules": true,           // Required by bundlers

    // Output
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,               // Emit .d.ts files
    "sourceMap": true,                 // Emit .js.map files
    "skipLibCheck": true               // Faster builds
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Common mistake: Setting target too low (e.g., ES5) when your runtime supports modern JS. This bloats output with unnecessary polyfills. Match your target to your minimum supported Node/browser version.

ESLint & Prettier

ESLint catches bugs and enforces patterns. Prettier handles formatting. Use both together.

# Install ESLint with TypeScript support
npm install -D eslint @eslint/js typescript-eslint

# Install Prettier
npm install -D prettier eslint-config-prettier
// eslint.config.mjs (flat config - ESLint v9+)
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  {
    languageOptions: {
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    ignores: ["dist/", "node_modules/"],
  }
);

Project Scaffolding

Quick-start commands for common project types.

npm init -y
Initialize a bare package.json. Add typescript and @types/node next.
npm create vite@latest
Scaffold a Vite project. Choose vanilla-ts, react-ts, or vue-ts template.
npx create-next-app@latest --ts
Full-stack Next.js with TypeScript, App Router, and Tailwind options.
npm init @eslint/config
Interactive ESLint configuration wizard.
03

Type System Fundamentals

Primitives & Basic Annotations

TypeScript adds static types on top of JavaScript. Type annotations come after a colon.

// Explicit annotations
let name: string = "Grace Hopper";
let year: number = 1906;
let isActive: boolean = true;

// Type inference — TS figures it out from the value
let language = "TypeScript";   // inferred as string
let version = 5.3;             // inferred as number
let features = ["generics", "enums"];  // inferred as string[]

// Arrays
let ids: number[] = [1, 2, 3];
let names: Array<string> = ["Ada", "Grace"];

// Tuples — fixed length, fixed types per position
let entry: [string, number] = ["age", 30];
let rgb: [number, number, number] = [255, 128, 0];

// Enum
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// const enum — inlined at compile time, no runtime object
const enum Status {
  Active = 1,
  Inactive = 0,
}
Best practice: Let TypeScript infer types when the value makes the type obvious. Add explicit annotations for function parameters, return types, and complex objects where inference might be ambiguous.

Unions & Intersections

Unions (|) mean "one of these types." Intersections (&) mean "all of these types combined."

// Union types — value can be either type
type StringOrNumber = string | number;
type Status = "pending" | "active" | "archived";

function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();   // TS knows it's string here
  }
  return value.toFixed(2);        // TS knows it's number here
}

// Intersection types — combine multiple types into one
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;

const user: Person = {
  name: "Alan",
  age: 41,
  // Must have ALL properties from both types
};

// Practical: extend API response types
type ApiResponse<T> = {
  data: T;
  timestamp: number;
} & (
  | { status: "success" }
  | { status: "error"; message: string }
);

Literal Types & const Assertions

Literal types narrow a value to a specific string, number, or boolean rather than the wider type.

// String literal types
type Theme = "light" | "dark" | "system";
let current: Theme = "dark";   // OK
// current = "blue";           // Error: not assignable to Theme

// Numeric literal types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

// Boolean literal
type True = true;

// const assertion — narrows to the most specific type
const config = {
  endpoint: "https://api.example.com",
  retries: 3,
  methods: ["GET", "POST"],
} as const;
// Type is: {
//   readonly endpoint: "https://api.example.com";
//   readonly retries: 3;
//   readonly methods: readonly ["GET", "POST"];
// }

// Without `as const`, methods would be string[]
// With `as const`, methods is readonly ["GET", "POST"]

// Template literal types
type EventName = `on${Capitalize<string>}`;
type CssUnit = `${number}${"px" | "rem" | "em" | "%"}`;

Type Narrowing

TypeScript narrows types based on control flow. Use type guards to help the compiler understand your code.

// typeof narrowing
function double(x: string | number) {
  if (typeof x === "string") {
    return x.repeat(2);       // string methods available
  }
  return x * 2;               // number operations available
}

// instanceof narrowing
function getDate(value: string | Date): Date {
  if (value instanceof Date) {
    return value;              // already a Date
  }
  return new Date(value);     // parse string to Date
}

// "in" operator narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim();             // Fish
  } else {
    animal.fly();              // Bird
  }
}

// Custom type guards — return type is a type predicate
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process(input: unknown) {
  if (isString(input)) {
    console.log(input.toUpperCase());  // safely narrowed
  }
}

// Truthiness narrowing
function printName(name: string | null | undefined) {
  if (name) {
    console.log(name.toUpperCase());   // string (not null/undefined)
  }
}

Discriminated Unions

The most powerful pattern for modeling variants. A shared literal field acts as the discriminant.

// Each variant has a `kind` field with a unique literal type
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

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 (shape.base * shape.height) / 2;
  }
}

// Exhaustiveness checking — catch missing cases at compile time
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

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}`;
    default:
      return assertNever(shape);  // Compile error if a case is missing
  }
}

// Real-world example: API response handling
type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: { code: number; message: string } };

function handleResult<T>(result: ApiResult<T>) {
  if (result.ok) {
    console.log(result.data);      // T — no error field here
  } else {
    console.error(result.error);   // error — no data field here
  }
}

Type Assertions & the satisfies Operator

Assertions override the compiler. satisfies validates a type without widening.

// Type assertion — "I know better than the compiler"
const canvas = document.getElementById("main") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!;  // non-null assertion

// Double assertion (escape hatch — avoid when possible)
const value = someExpression as unknown as TargetType;

// The `satisfies` operator (TS 5.0+) — validate without widening
type ColorMap = Record<string, [number, number, number] | string>;

// With `satisfies`, TS validates the shape but keeps the narrow type
const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies ColorMap;

// palette.red is [number, number, number], NOT string | [...]
// palette.green is string, NOT string | [...]
palette.red.map(c => c / 255);   // OK — TS knows it's a tuple
palette.green.toUpperCase();       // OK — TS knows it's a string

// Without satisfies, using `as ColorMap` would widen:
// palette.red would be string | [number, number, number]
// and you'd lose the ability to call .map() without narrowing
Avoid overusing assertions. Every as is a potential runtime error. Prefer type guards and narrowing. Use satisfies when you want validation without losing type precision.
04

Functions & Control Flow

Arrow Functions vs Declarations

Both forms define functions, but they differ in this binding and hoisting behavior.

// Function declaration — hoisted, has its own `this`
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// Arrow function — NOT hoisted, inherits `this` from enclosing scope
const greetArrow = (name: string): string => {
  return `Hello, ${name}!`;
};

// Concise body — implicit return for single expressions
const double = (n: number): number => n * 2;

// Typing a function variable separately
type Formatter = (input: string, width: number) => string;

const padLeft: Formatter = (input, width) => {
  return input.padStart(width);
};

// Generic arrow function (note the trailing comma in TSX files)
const identity = <T,>(value: T): T => value;
When to choose: Use declarations for top-level named functions (hoisting helps readability). Use arrows for callbacks, closures, and anywhere you need lexical this.

Function Overloads

Overloads let a single function handle different argument shapes with precise return types. TS Only

// Overload signatures (no body)
function createElement(tag: "a"): HTMLAnchorElement;
function createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement;

// Implementation signature (must be compatible with ALL overloads)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const link = createElement("a");       // HTMLAnchorElement
const canvas = createElement("canvas"); // HTMLCanvasElement
const div = createElement("div");       // HTMLDivElement
const span = createElement("span");     // HTMLElement

// Practical: parse function that returns different types
function parse(input: string, format: "json"): object;
function parse(input: string, format: "csv"): string[][];
function parse(input: string, format: "json" | "csv"): object | string[][] {
  if (format === "json") return JSON.parse(input);
  return input.split("\n").map(row => row.split(","));
}
Alternative: Often a discriminated union return type or generic can replace overloads with less boilerplate. Use overloads when the return type truly depends on specific argument values.

Destructuring & Rest/Spread

Destructure parameters directly in the function signature for cleaner APIs.

// Object destructuring with defaults and types
function createUser({
  name,
  role = "viewer",
  active = true,
}: {
  name: string;
  role?: "admin" | "editor" | "viewer";
  active?: boolean;
}): User {
  return { id: crypto.randomUUID(), name, role, active };
}

createUser({ name: "Ada" });
createUser({ name: "Grace", role: "admin" });

// Array destructuring in parameters
function getFirst<T>([head, ...rest]: [T, ...T[]]): T {
  return head;
}

// Rest parameters — typed as an array
function log(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, ...messages);
}

// Variadic tuple types (TS 4.0+) — spread tuples in type signatures
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
type Result = Concat<[1, 2], [3, 4]>;  // [1, 2, 3, 4]

// Spread in function calls
const nums = [1, 2, 3] as const;
Math.max(...nums);  // `as const` makes it a tuple so spread works

Optional Chaining & Nullish Coalescing

Safe property access and default values without verbose null checks. TS + JS

// Optional chaining (?.) — short-circuits to undefined
const city = user?.address?.city;           // undefined if any link is null/undefined
const first = users?.[0]?.name;             // safe array access
const result = callback?.();                // safe function call

// Nullish coalescing (??) — default only for null/undefined (not 0 or "")
const port = config.port ?? 3000;           // 0 stays 0, null becomes 3000
const name = input ?? "Anonymous";

// Compare with || which treats 0, "", false as falsy
const count = data.count || 10;   // BUG: count of 0 becomes 10
const count = data.count ?? 10;   // CORRECT: count of 0 stays 0

// Nullish coalescing assignment (??=)
let cache: string[] | undefined;
cache ??= [];                                // assign only if null/undefined
cache.push("item");

// Chaining them together — real-world config
const timeout = options?.network?.timeout ?? defaults?.timeout ?? 5000;
Watch out: ?. always returns undefined when it short-circuits, never null. This matters when distinguishing between "property is null" vs "property path doesn't exist."

Iterators & Generators

Generators produce values lazily on demand. Async generators handle streams of async data.

// Generator function — yields values one at a time
function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) {
    yield i;
  }
}

for (const n of range(0, 5)) {
  console.log(n);  // 0, 1, 2, 3, 4
}

// Spread a generator into an array
const nums = [...range(1, 4)];  // [1, 2, 3]

// Infinite generator with early termination
function* fibonacci(): Generator<number> {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Take first N values from any iterable
function take<T>(n: number, iter: Iterable<T>): T[] {
  const result: T[] = [];
  for (const item of iter) {
    result.push(item);
    if (result.length >= n) break;
  }
  return result;
}

take(8, fibonacci());  // [0, 1, 1, 2, 3, 5, 8, 13]

// Async generator — stream paginated API data
async function* fetchPages<T>(url: string): AsyncGenerator<T[]> {
  let nextUrl: string | null = url;
  while (nextUrl) {
    const res = await fetch(nextUrl);
    const json = await res.json();
    yield json.data;
    nextUrl = json.nextPage;
  }
}

// Consume with for-await-of
for await (const page of fetchPages<User>("/api/users")) {
  for (const user of page) {
    processUser(user);
  }
}
05

Objects, Classes & Interfaces

interface vs type

Both can describe object shapes, but they have key differences.

Feature interface type
Declaration merging Yes — multiple declarations auto-merge No — duplicate identifiers are an error
extends interface B extends A type B = A & { ... } (intersection)
Unions Not supported type X = A | B
Primitives, tuples, mapped types Not supported Full support
implements Yes Yes (for object types)
Computed properties No Yes (mapped types, template literals)
Error messages Named in errors (more readable) Inlined/expanded (can be verbose)
// Interface — best for object shapes and public APIs
interface User {
  id: string;
  name: string;
  email: string;
}

// Declaration merging — extend third-party types
interface Window {
  analytics: AnalyticsSDK;  // adds to the global Window interface
}

// Type alias — best for unions, tuples, computed types
type Status = "active" | "inactive" | "pending";
type Pair<T> = [T, T];
type StringKeys<T> = { [K in keyof T as K extends string ? K : never]: T[K] };
Rule of thumb: Use interface for objects that will be extended or implemented. Use type for unions, intersections, tuples, and computed types. Both work for plain object shapes — pick one and be consistent.

Classes

TypeScript adds access modifiers, abstract classes, and parameter properties to JavaScript classes.

class EventEmitter<Events extends Record<string, unknown[]>> {
  // Private field — not accessible outside the class
  private listeners = new Map<keyof Events, Set<Function>>();

  // Protected — accessible in subclasses
  protected maxListeners: number = 10;

  // Readonly — set once in constructor
  readonly name: string;

  // Parameter property shorthand — declares + assigns in constructor
  constructor(name: string, private debug: boolean = false) {
    this.name = name;
  }

  on<K extends keyof Events>(
    event: K,
    listener: (...args: Events[K]) => void
  ): this {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set());
    }
    this.listeners.get(event)!.add(listener);
    if (this.debug) console.log(`Listener added for ${String(event)}`);
    return this;  // enables chaining
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.listeners.get(event)?.forEach(fn => fn(...args));
  }
}

// Static members
class IdGenerator {
  static #counter = 0;                   // private static (ES2022)
  static next(): string {
    return `id_${++IdGenerator.#counter}`;
  }
}

// Abstract class — cannot be instantiated directly
abstract class Shape {
  abstract area(): number;
  abstract perimeter(): number;

  describe(): string {
    return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().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; }
}

// implements — class must satisfy the interface contract
interface Serializable {
  toJSON(): string;
}

class Config implements Serializable {
  constructor(private data: Record<string, unknown>) {}
  toJSON(): string { return JSON.stringify(this.data); }
}

Generics

Generics let you write reusable code that works with any type while preserving type safety. TS Only

// Basic generic function
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
first([1, 2, 3]);         // number | undefined
first(["a", "b"]);        // string | undefined

// Constrained generic — T must have a `.length` property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest("hello", "hi");   // "hello" (return type is string)
longest([1, 2], [3]);     // [1, 2] (return type is number[])

// Generic with default type parameter
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  headers: Record<string, string>;
}

const res: ApiResponse = { data: null, status: 200, headers: {} };
const typed: ApiResponse<User[]> = { data: users, status: 200, headers: {} };

// const type parameters (TS 5.0) — infer literal types automatically
function routes<const T extends readonly string[]>(paths: T): T {
  return paths;
}
const r = routes(["/home", "/about"]);
// Type is readonly ["/home", "/about"], not string[]

// Multiple generic parameters with constraint relationship
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
getProperty({ name: "Ada", age: 36 }, "name");  // string

Mapped & Conditional Types

Transform existing types into new shapes programmatically. TS Only

// Basic mapped type — make all properties optional
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Mapped type with key remapping via `as` (TS 4.1)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

// Filter properties with never
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringProps = OnlyStrings<{ name: string; age: number; email: string }>;
// { name: string; email: string }

// Conditional types — type-level if/else
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;   // true
type B = IsString<42>;        // false

// The `infer` keyword — extract types from within other types
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type X = UnwrapPromise<Promise<string>>;   // string
type Y = UnwrapPromise<number>;             // number

// Extract function parameter types
type FirstParam<F> = F extends (first: infer P, ...rest: any[]) => any ? P : never;
type P = FirstParam<(name: string, age: number) => void>;  // string

// Template literal types — string manipulation at the type level
type EventName<T extends string> = `${T}Changed`;
type UserEvents = EventName<"name" | "email">;
// "nameChanged" | "emailChanged"

type CSSProperty = `${"margin" | "padding"}-${"top" | "right" | "bottom" | "left"}`;
// "margin-top" | "margin-right" | ... | "padding-left" (8 combinations)
Distributive behavior: Conditional types distribute over unions automatically. IsString<"a" | 42> evaluates to true | false (which simplifies to boolean). Wrap in a tuple [T] extends [string] to prevent distribution.
06

Async Patterns

Promises & async/await

Promises represent eventual values. async/await is syntactic sugar that makes async code read like synchronous code.

// Creating a Promise
function delay(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// Promise chaining
fetch("/api/users")
  .then(res => res.json())
  .then(users => console.log(users))
  .catch(err => console.error("Failed:", err))
  .finally(() => console.log("Done"));

// async/await — equivalent to the chain above
async function loadUsers(): Promise<User[]> {
  try {
    const res = await fetch("/api/users");
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("Failed:", err);
    return [];
  }
}

// Error handling pattern — Result tuple instead of try/catch
async function safeAsync<T>(
  promise: Promise<T>
): Promise<[T, null] | [null, Error]> {
  try {
    return [await promise, null];
  } catch (err) {
    return [null, err instanceof Error ? err : new Error(String(err))];
  }
}

const [users, error] = await safeAsync(loadUsers());
if (error) {
  console.error(error.message);
}

Promise Combinators

Run multiple async operations in parallel with different strategies.

Combinator Resolves when Rejects when Use case
Promise.all ALL promises fulfill ANY promise rejects Fetch multiple resources in parallel
Promise.allSettled ALL promises settle (fulfill or reject) Never rejects Fire-and-forget batch operations
Promise.race FIRST promise settles FIRST settled is a rejection Timeout pattern, fastest mirror
Promise.any FIRST promise fulfills ALL promises reject (AggregateError) Fastest success from redundant sources
// Promise.all — parallel fetch, fail-fast
const [users, posts, comments] = await Promise.all([
  fetch("/api/users").then(r => r.json()),
  fetch("/api/posts").then(r => r.json()),
  fetch("/api/comments").then(r => r.json()),
]);

// Promise.allSettled — collect all results regardless of failures
const results = await Promise.allSettled([
  sendEmail("user1@example.com"),
  sendEmail("user2@example.com"),
  sendEmail("invalid"),
]);

for (const result of results) {
  if (result.status === "fulfilled") {
    console.log("Sent:", result.value);
  } else {
    console.error("Failed:", result.reason);
  }
}

AbortController & Timeouts

Cancel in-flight requests and set timeouts using the standard AbortController API.

// Basic abort
const controller = new AbortController();
const { signal } = controller;

fetch("/api/data", { signal })
  .then(res => res.json())
  .catch(err => {
    if (err.name === "AbortError") {
      console.log("Request was cancelled");
    }
  });

// Cancel after user action
cancelButton.addEventListener("click", () => controller.abort());

// AbortSignal.timeout() — auto-cancel after duration
const res = await fetch("/api/slow", {
  signal: AbortSignal.timeout(5000),   // 5-second timeout
});

// Combine signals — cancel on timeout OR user action
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);

const res = await fetch("/api/data", {
  signal: AbortSignal.any([userController.signal, timeoutSignal]),
});

// Timeout wrapper for any promise
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    promise,
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
    ),
  ]);
}

Event Loop: Microtasks vs Macrotasks

Understanding execution order is essential for debugging async behavior.

// Execution order demonstration
console.log("1: synchronous");

setTimeout(() => console.log("2: macrotask (setTimeout)"), 0);

Promise.resolve().then(() => console.log("3: microtask (Promise)"));

queueMicrotask(() => console.log("4: microtask (queueMicrotask)"));

console.log("5: synchronous");

// Output order: 1, 5, 3, 4, 2
// Synchronous first, then ALL microtasks, then next macrotask
Queue APIs Priority
Microtasks Promise.then, queueMicrotask, MutationObserver Run after current task, before next macrotask
Macrotasks setTimeout, setInterval, setImmediate, I/O callbacks Run one per event loop iteration
Gotcha: An infinite chain of microtasks will starve macrotasks. await in a tight loop creates microtasks, not macrotasks. If you need to yield to the event loop, use setTimeout(fn, 0) explicitly.

Practical Async Patterns

Battle-tested patterns for real-world async code.

// Retry with exponential backoff
async function retry<T>(
  fn: () => Promise<T>,
  { attempts = 3, delayMs = 1000, backoff = 2 } = {}
): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
      const wait = delayMs * backoff ** i;
      await new Promise(r => setTimeout(r, wait));
    }
  }
  throw new Error("Unreachable");
}

const data = await retry(() => fetch("/flaky-api").then(r => r.json()));

// Concurrent limiting — run N promises at a time
async function mapConcurrent<T, R>(
  items: T[],
  fn: (item: T) => Promise<R>,
  concurrency: number
): Promise<R[]> {
  const results: R[] = [];
  const executing = new Set<Promise<void>>();

  for (const [i, item] of items.entries()) {
    const p = fn(item).then(result => { results[i] = result; });
    executing.add(p);
    p.finally(() => executing.delete(p));

    if (executing.size >= concurrency) {
      await Promise.race(executing);
    }
  }
  await Promise.all(executing);
  return results;
}

// Process 100 URLs, max 5 at a time
const pages = await mapConcurrent(urls, fetchPage, 5);

// Top-level await (ES2022 modules)
const config = await loadConfig();
const db = await connectDatabase(config.dbUrl);
export { db };
Top-level await works in ES modules (type: "module" in package.json or .mjs files). It blocks importing modules from executing until the awaited promise resolves, so use it sparingly at the application entry point.
07

Modules & Module Systems

ESM vs CJS Comparison

JavaScript has two module systems. ESM (ECMAScript Modules) is the standard; CJS (CommonJS) is the Node.js legacy format.

Feature ESM CJS
Syntax import / export require() / module.exports
Loading Asynchronous, statically analyzed Synchronous, evaluated at runtime
Top-level this undefined module.exports
File extensions .mjs or .js with "type": "module" .cjs or .js (default in Node)
Tree-shaking Yes — static analysis enables dead-code elimination No — dynamic require() prevents it
Top-level await Supported Not supported
__dirname / __filename Not available — use import.meta.url Available as globals
Current best practice: Default to ESM for new projects. Set "type": "module" in package.json. Use .cjs for any files that must remain CommonJS (e.g., legacy config files).

ESM: import/export Patterns

Static imports are analyzed at parse time before any code executes, enabling optimizations like tree-shaking.

// Named exports — one or many per file
export const API_URL = "https://api.example.com";
export function fetchUser(id: string): Promise<User> { /* ... */ }
export class UserService { /* ... */ }

// Named imports
import { API_URL, fetchUser } from "./api.js";

// Rename on import
import { fetchUser as getUser } from "./api.js";

// Default export — one per file, represents the module's main value
export default class Router { /* ... */ }

// Default import — any name works
import Router from "./router.js";
import MyRouter from "./router.js";  // same thing, different name

// Namespace import — grab everything as an object
import * as utils from "./utils.js";
utils.formatDate(new Date());

// Re-exports — aggregate modules into a single entry point
export { fetchUser, UserService } from "./user.js";
export { default as Router } from "./router.js";
export * from "./helpers.js";           // re-export all named exports
export * as math from "./math.js";      // re-export as namespace

// Side-effect import — runs the module but imports nothing
import "./polyfills.js";
import "./styles.css";  // common in bundled projects

CJS & ESM Interop

Mixing module systems is common when migrating or consuming legacy packages. Here are the key interop rules.

// CJS syntax — require is synchronous
const fs = require("fs");
const { readFile, writeFile } = require("fs/promises");

// CJS exports
module.exports = { fetchUser, createUser };
module.exports = class Database { /* ... */ };

// ESM importing a CJS module (usually works)
import express from "express";            // CJS default
import { Router } from "express";         // named — depends on package

// CJS requiring an ESM module (NOT directly supported)
// Must use dynamic import() instead:
async function loadESM() {
  const { default: chalk } = await import("chalk");  // chalk is ESM-only
  console.log(chalk.blue("Hello"));
}

// __dirname equivalent in ESM
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Or use import.meta.dirname (Node 21.2+)
const dir = import.meta.dirname;  // simpler
Watch out: CJS modules do not have named exports from the ESM perspective. When you write import { x } from "cjs-pkg", Node performs "named export detection" as a best-effort heuristic. If it fails, fall back to import pkg from "cjs-pkg" and destructure from pkg.

Dynamic import() & Code Splitting

import() returns a Promise and works in both ESM and CJS. Bundlers use it as a split point.

// Lazy-load a heavy module on demand
const button = document.getElementById("chart-btn")!;
button.addEventListener("click", async () => {
  const { Chart } = await import("chart.js");
  const chart = new Chart(canvas, config);
});

// Conditional import — load platform-specific code
const db = process.env.NODE_ENV === "test"
  ? await import("./db-mock.js")
  : await import("./db-postgres.js");

// Route-level code splitting (framework-agnostic pattern)
const routes: Record<string, () => Promise<{ default: Component }>> = {
  "/":        () => import("./pages/home.js"),
  "/about":   () => import("./pages/about.js"),
  "/contact": () => import("./pages/contact.js"),
};

async function navigate(path: string) {
  const loader = routes[path];
  if (!loader) return show404();
  const { default: Page } = await loader();
  render(Page);
}

// Type-only imports — stripped at compile time, zero runtime cost
import type { User, Config } from "./types.js";
import { fetchUser, type ApiResponse } from "./api.js";
Best practice: Use import type for anything you only need at the type level. It prevents accidental side-effect imports and makes bundler output smaller.

Package Configuration & Barrel Files

The package.json "exports" field controls what consumers can import, replacing the flat "main" field.

// package.json — dual ESM/CJS package
{
  "name": "my-lib",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",      // TypeScript types (must be first)
      "import": "./dist/index.js",        // ESM entry
      "require": "./dist/index.cjs"       // CJS entry
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    }
  },
  "files": ["dist"]
}

Barrel files re-export from a single index.ts to simplify imports. Use them carefully.

// src/components/index.ts — barrel file
export { Button } from "./Button.js";
export { Modal } from "./Modal.js";
export { Tooltip } from "./Tooltip.js";

// Consumer gets clean imports
import { Button, Modal } from "@my-lib/components";
Barrel file tradeoff: Barrels simplify imports but can harm tree-shaking. Importing one item from a barrel may pull in all modules if they have side effects. For libraries, prefer direct deep imports or use the "exports" field to expose specific subpaths. Mark your package "sideEffects": false to help bundlers eliminate unused code.

Bundlers Overview

Bundlers combine modules into optimized files for the browser or edge runtimes.

Bundler Language Best For Dev Server
Vite JS + esbuild + Rollup Frontend apps, frameworks. Instant HMR via native ESM. Built-in, blazing fast
esbuild Go Libraries, CLI tools. 10-100x faster than webpack. Minimal (use with Vite)
Rollup JS Libraries. Best tree-shaking. Output is clean, readable. Plugin-based
webpack JS Legacy apps, complex asset pipelines. Massive plugin ecosystem. webpack-dev-server
tsup JS (esbuild) TypeScript libraries. Zero-config, outputs ESM + CJS + .d.ts. N/A (library bundler)
Starting a new project? Use Vite for apps and tsup for libraries. Both have sensible defaults and minimal configuration. Reach for webpack only if you need its specific plugin ecosystem.
08

DOM & Browser APIs

DOM Selection & Typing

Query the DOM safely with TypeScript by asserting or narrowing the returned element type.

// querySelector returns Element | null — narrow with type assertion
const canvas = document.querySelector<HTMLCanvasElement>("#game");
// Type is HTMLCanvasElement | null

// Non-null assertion when you know the element exists
const app = document.getElementById("app")!;

// Safer pattern — check at runtime
const form = document.querySelector<HTMLFormElement>("#login-form");
if (!form) throw new Error("Login form not found");
// form is HTMLFormElement from here on

// querySelectorAll returns NodeListOf<Element>
const buttons = document.querySelectorAll<HTMLButtonElement>(".btn");
buttons.forEach(btn => btn.disabled = true);

// Convert NodeList to array for array methods
const items = [...document.querySelectorAll<HTMLLIElement>(".item")];
const visible = items.filter(el => !el.hidden);

// closest() — traverse up the DOM tree
const row = clickedCell.closest<HTMLTableRowElement>("tr");

// Creating elements with type safety
const div = document.createElement("div");    // HTMLDivElement
const link = document.createElement("a");     // HTMLAnchorElement
link.href = "/about";                          // type-checked property

Event Handling & Delegation

TypeScript provides strong typing for DOM events. Use event delegation for dynamic content.

// Basic event listener with typed event
const button = document.querySelector<HTMLButtonElement>("#save")!;

button.addEventListener("click", (event: MouseEvent) => {
  event.preventDefault();
  console.log(event.clientX, event.clientY);
});

// Input events — access .value safely
const input = document.querySelector<HTMLInputElement>("#search")!;
input.addEventListener("input", (e) => {
  const target = e.target as HTMLInputElement;
  console.log(target.value);       // the current text
});

// Event delegation — one listener handles all children
const list = document.querySelector<HTMLUListElement>("#todo-list")!;
list.addEventListener("click", (e) => {
  const target = e.target as HTMLElement;
  const item = target.closest<HTMLLIElement>("li[data-id]");
  if (!item) return;  // click wasn't on a list item
  const id = item.dataset.id!;
  toggleTodo(id);
});

// Custom events — typed payloads
interface NotifyDetail { message: string; level: "info" | "error" }
const event = new CustomEvent<NotifyDetail>("notify", {
  detail: { message: "Saved!", level: "info" },
  bubbles: true,
});
document.dispatchEvent(event);

// Remove listeners — keep a reference to the handler
const handler = (e: KeyboardEvent) => {
  if (e.key === "Escape") closeModal();
};
document.addEventListener("keydown", handler);
document.removeEventListener("keydown", handler);

// AbortController for listener cleanup (modern approach)
const controller = new AbortController();
document.addEventListener("keydown", handler, { signal: controller.signal });
document.addEventListener("click", clickHandler, { signal: controller.signal });
// Remove ALL listeners at once:
controller.abort();
Cleanup pattern: Using AbortController to manage event listeners is cleaner than tracking individual references. Create one controller per component lifecycle and abort it on teardown.

Fetch API with TypeScript

Type your API responses and handle errors properly. Fetch does not reject on HTTP errors.

// Typed fetch wrapper
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchJSON<T>(url: string, init?: RequestInit): Promise<T> {
  const res = await fetch(url, init);
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}: ${res.statusText}`);
  }
  return res.json() as Promise<T>;
}

const user = await fetchJSON<User>("/api/users/1");
console.log(user.name);  // string — fully typed

// POST with JSON body
const created = await fetchJSON<User>("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Ada", email: "ada@example.com" }),
});

// Fetch with AbortController for timeout
async function fetchWithTimeout<T>(url: string, ms = 5000): Promise<T> {
  const res = await fetch(url, { signal: AbortSignal.timeout(ms) });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<T>;
}

// Streaming a response body
async function streamText(url: string): Promise<string> {
  const res = await fetch(url);
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let result = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += decoder.decode(value, { stream: true });
  }
  return result;
}

Web Storage & Modern APIs

Typed wrappers around browser storage and commonly-used observer APIs.

// Typed localStorage wrapper
function getStored<T>(key: string, fallback: T): T {
  const raw = localStorage.getItem(key);
  if (raw === null) return fallback;
  try {
    return JSON.parse(raw) as T;
  } catch {
    return fallback;
  }
}

function setStored<T>(key: string, value: T): void {
  localStorage.setItem(key, JSON.stringify(value));
}

// Usage
const prefs = getStored<{ theme: string; lang: string }>(
  "prefs", { theme: "dark", lang: "en" }
);

// IntersectionObserver — lazy loading, infinite scroll, animations
const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) {
        const img = entry.target as HTMLImageElement;
        img.src = img.dataset.src!;
        observer.unobserve(img);    // stop watching after load
      }
    }
  },
  { rootMargin: "200px" }          // start loading 200px before visible
);
document.querySelectorAll<HTMLImageElement>("img[data-src]")
  .forEach(img => observer.observe(img));

// ResizeObserver — respond to element size changes
const resizer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    console.log(`Resized to ${width}x${height}`);
  }
});
resizer.observe(document.getElementById("panel")!);

// MutationObserver — watch for DOM changes
const mutation = new MutationObserver((mutations) => {
  for (const m of mutations) {
    console.log(`${m.type}: ${m.addedNodes.length} added`);
  }
});
mutation.observe(document.body, { childList: true, subtree: true });

Canvas, URL & Utility APIs

Commonly used browser APIs for graphics, URL manipulation, and data handling.

// Canvas 2D basics
const canvas = document.querySelector<HTMLCanvasElement>("#game")!;
const ctx = canvas.getContext("2d")!;

ctx.fillStyle = "#ff2975";
ctx.fillRect(10, 10, 100, 50);

ctx.strokeStyle = "#00d4ff";
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(200, 100, 40, 0, Math.PI * 2);
ctx.stroke();

// Animation loop
function gameLoop(timestamp: number) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  update(timestamp);
  draw(ctx);
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);

// URLSearchParams — parse and build query strings
const params = new URLSearchParams(window.location.search);
const page = Number(params.get("page") ?? "1");
const query = params.get("q") ?? "";

params.set("page", "2");
history.pushState(null, "", `?${params}`);

// FormData — read form values without manual extraction
const form = document.querySelector<HTMLFormElement>("#signup")!;
form.addEventListener("submit", (e) => {
  e.preventDefault();
  const data = new FormData(form);
  const name = data.get("name") as string;
  const file = data.get("avatar") as File;

  // Send as multipart/form-data
  fetch("/api/signup", { method: "POST", body: data });
});

// structuredClone — deep clone any structured-cloneable value
const original = { nested: { items: [1, 2, 3] }, date: new Date() };
const cloned = structuredClone(original);
cloned.nested.items.push(4);  // does not affect original
structuredClone handles Date, Map, Set, ArrayBuffer, and nested objects — unlike JSON.parse(JSON.stringify()) which loses non-JSON types. Available in all modern browsers and Node 17+.
09

Node.js Runtime

File System (fs/promises)

Always use the promise-based API from node:fs/promises. The callback API is legacy.

import { readFile, writeFile, readdir, stat, mkdir, rm }
  from "node:fs/promises";
import { join } from "node:path";

// Read a file as text
const content = await readFile("config.json", "utf-8");
const config = JSON.parse(content);

// Write a file (creates or overwrites)
await writeFile("output.json", JSON.stringify(data, null, 2));

// Read directory contents
const entries = await readdir("./src", { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
const files = entries.filter(e => e.isFile()).map(e => e.name);

// Recursive directory listing
const allFiles = await readdir("./src", { recursive: true });
// Returns: ["index.ts", "utils/math.ts", "utils/string.ts", ...]

// File metadata
const info = await stat("package.json");
console.log(`Size: ${info.size} bytes`);
console.log(`Modified: ${info.mtime.toISOString()}`);
console.log(`Is file: ${info.isFile()}`);

// Create directory tree (recursive)
await mkdir("dist/assets/images", { recursive: true });

// Delete directory tree
await rm("dist", { recursive: true, force: true });

// Check if a file exists (without throwing)
import { access, constants } from "node:fs/promises";
async function fileExists(path: string): Promise<boolean> {
  try {
    await access(path, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

Path Module

Never concatenate paths with string templates. Use node:path for cross-platform safety.

import { join, resolve, dirname, basename, extname, relative, parse }
  from "node:path";

// join — combine segments, normalize separators
join("src", "utils", "math.ts");
// "src/utils/math.ts" (Linux)  "src\\utils\\math.ts" (Windows)

// resolve — build absolute path from CWD
resolve("src", "index.ts");
// "/home/user/project/src/index.ts"

// dirname, basename, extname — decompose paths
dirname("/app/src/index.ts");    // "/app/src"
basename("/app/src/index.ts");   // "index.ts"
basename("/app/src/index.ts", ".ts");  // "index"
extname("/app/src/index.ts");    // ".ts"

// parse — all components at once
parse("/app/src/index.ts");
// { root: "/", dir: "/app/src", base: "index.ts",
//   ext: ".ts", name: "index" }

// relative — path from A to B
relative("/app/src", "/app/dist/bundle.js");
// "../dist/bundle.js"

// ESM: get current file's directory
import { fileURLToPath } from "node:url";
const __dirname = import.meta.dirname;    // Node 21.2+
// Or: dirname(fileURLToPath(import.meta.url));

HTTP Server & Streams

Node's built-in HTTP server and stream primitives for handling requests and data pipelines.

import { createServer } from "node:http";
import { createReadStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

// Minimal HTTP server with routing
const server = createServer(async (req, res) => {
  const url = new URL(req.url!, `http://${req.headers.host}`);

  if (req.method === "GET" && url.pathname === "/api/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok" }));
    return;
  }

  if (req.method === "POST" && url.pathname === "/api/data") {
    // Read request body
    const chunks: Buffer[] = [];
    for await (const chunk of req) chunks.push(chunk as Buffer);
    const body = JSON.parse(Buffer.concat(chunks).toString());
    res.writeHead(201, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ received: body }));
    return;
  }

  res.writeHead(404).end("Not Found");
});

server.listen(3000, () => console.log("Listening on :3000"));

// Streams — pipe a file through gzip to response
async function serveGzipped(filePath: string, res: ServerResponse) {
  res.writeHead(200, {
    "Content-Type": "text/html",
    "Content-Encoding": "gzip",
  });
  await pipeline(
    createReadStream(filePath),
    createGzip(),
    res
  );
}

// Transform stream — process data line by line
import { Transform } from "node:stream";

const uppercase = new Transform({
  transform(chunk, encoding, callback) {
    callback(null, chunk.toString().toUpperCase());
  },
});

await pipeline(
  createReadStream("input.txt"),
  uppercase,
  createWriteStream("output.txt")
);
Always use pipeline() instead of .pipe(). Pipeline properly handles error propagation and cleanup of all streams in the chain. The promise version from node:stream/promises works with async/await.

Child Processes

Run external commands from Node using node:child_process.

import { exec, execFile, spawn } from "node:child_process";
import { promisify } from "node:util";

const execAsync = promisify(exec);

// exec — run a shell command, buffer output
const { stdout, stderr } = await execAsync("git status --short");
console.log(stdout);

// execFile — safer, no shell interpretation (no injection risk)
const execFileAsync = promisify(execFile);
const { stdout: version } = await execFileAsync("node", ["--version"]);

// spawn — streaming output for long-running processes
import { spawn } from "node:child_process";

const child = spawn("npm", ["run", "build"], {
  stdio: "inherit",   // pipe child's stdio to parent
  cwd: "/path/to/project",
  env: { ...process.env, NODE_ENV: "production" },
});

child.on("exit", (code) => {
  console.log(`Build exited with code ${code}`);
});

// spawn with piped output — process line by line
const proc = spawn("grep", ["-r", "TODO", "src/"]);
proc.stdout.on("data", (data: Buffer) => {
  for (const line of data.toString().split("\n")) {
    if (line.trim()) console.log("Found:", line);
  }
});
proc.on("close", (code) => console.log("Done:", code));
Security: Prefer execFile or spawn over exec when handling user input. exec runs through a shell, making it vulnerable to command injection. execFile and spawn pass arguments as an array, bypassing shell interpretation.

Environment & Debugging

Environment variables, CLI arguments, and modern Node.js debugging and development tools.

// Environment variables
const port = Number(process.env.PORT ?? "3000");
const dbUrl = process.env.DATABASE_URL;
if (!dbUrl) throw new Error("DATABASE_URL is required");

// .env files — built-in support (Node 22+, --env-file flag)
// node --env-file=.env app.js
// node --env-file=.env --env-file=.env.local app.js  // multiple files

// .env (loaded automatically with --env-file)
// DATABASE_URL=postgres://localhost:5432/mydb
// PORT=8080
// NODE_ENV=production

// CLI arguments
const args = process.argv.slice(2);   // skip node + script path
// node script.js --port 3000 --verbose
// args = ["--port", "3000", "--verbose"]

// parseArgs (Node 18.3+ built-in, no library needed)
import { parseArgs } from "node:util";
const { values, positionals } = parseArgs({
  args: process.argv.slice(2),
  options: {
    port: { type: "string", short: "p", default: "3000" },
    verbose: { type: "boolean", short: "v", default: false },
  },
  allowPositionals: true,
});
console.log(values.port, values.verbose, positionals);

// Debugging: start with --inspect, open chrome://inspect
// node --inspect app.js          # break on debugger statements
// node --inspect-brk app.js      # break on first line

// Watch mode (Node 22+ stable, 18+ experimental)
// node --watch app.js             # restart on file changes
// node --watch-path=src app.js    # watch specific directory

// Built-in test runner (Node 22+ stable)
// node --test                     # discover and run test files
// node --test --test-reporter=spec  # detailed output
// node --test "**/*.test.ts"      # glob pattern for test files
Node 22+ ships with: built-in .env loading (--env-file), stable watch mode (--watch), a built-in test runner (--test), glob support, and WebSocket client. These features reduce the need for dotenv, nodemon, jest/vitest for simple projects, and glob/fast-glob packages.
10

Advanced TypeScript

Decorators (Stage 3, TS 5.0+)

TC39 Stage 3 decorators are natively supported in TypeScript 5.0+ without experimental flags. They replace the legacy experimentalDecorators option with a standards-based approach. TS Only

// Class decorator — wraps or replaces the class
function sealed(constructor: Function, _context: ClassDecoratorContext) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Config {
  host = "localhost";
  port = 3000;
}

// Method decorator — intercept calls, add logging, etc.
function log(
  target: Function,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${String(context.name)} with`, args);
    const result = target.apply(this, args);
    console.log(`${String(context.name)} returned`, result);
    return result;
  };
}

class MathService {
  @log
  add(a: number, b: number) { return a + b; }
}

// Auto-accessor keyword (TS 5.0+) — generates getter/setter pair
class User {
  @validate
  accessor name: string = "";  // auto-accessor creates backing storage
}

function validate<T>(
  target: ClassAccessorDecoratorTarget<unknown, T>,
  context: ClassAccessorDecoratorContext
): ClassAccessorDecoratorResult<unknown, T> {
  return {
    set(value: T) {
      if (typeof value === "string" && !value.trim()) {
        throw new Error(`${String(context.name)} cannot be empty`);
      }
      target.set.call(this, value);
    },
  };
}

// Decorator metadata (TS 5.2+) — attach info at decoration time
Symbol.metadata ??= Symbol("Symbol.metadata");

function track(target: Function, context: ClassMethodDecoratorContext) {
  context.metadata.tracked ??= [];
  (context.metadata.tracked as string[]).push(String(context.name));
}

class Analytics {
  @track processEvent() {}
  @track sendReport() {}
}
// Analytics[Symbol.metadata].tracked → ["processEvent", "sendReport"]

Declaration Files & Ambient Modules

Declaration files (.d.ts) describe the shape of JavaScript libraries so TypeScript can type-check code that consumes them. TS Only

// global.d.ts — declare globals available everywhere
declare const __DEV__: boolean;
declare const API_URL: string;

// Ambient module — type an untyped npm package
// types/legacy-lib.d.ts
declare module "legacy-lib" {
  export function parse(input: string): Record<string, unknown>;
  export function stringify(obj: unknown): string;
  export const version: string;
}

// Wildcard module — handle non-JS imports
declare module "*.svg" {
  const content: string;
  export default content;
}
declare module "*.css" {
  const classes: Record<string, string>;
  export default classes;
}

// Triple-slash directives — reference other declarations
/// <reference types="vite/client" />
/// <reference path="./custom-globals.d.ts" />

// Namespace in .d.ts — group related types
declare namespace Express {
  interface Request {
    user?: { id: string; role: string };
    sessionId: string;
  }
}
Tip: Use declare module for typing third-party packages that lack types. Create a types/ directory and add it to typeRoots or paths in tsconfig.json.

Module Augmentation & Namespace Merging

Extend existing module types and interfaces without modifying the original source — essential for adding custom fields to libraries. TS Only

// Augment Express Request with custom properties
// types/express.d.ts
import "express";
declare module "express" {
  interface Request {
    user: { id: string; email: string; role: "admin" | "user" };
    requestId: string;
  }
}

// Augment Window with custom globals
// types/global.d.ts
declare global {
  interface Window {
    analytics: { track(event: string, data?: object): void };
    featureFlags: Map<string, boolean>;
  }
}
export {};  // required: makes this file a module

// Augment an existing library's types
import "zod";
declare module "zod" {
  interface ZodString {
    slug(): ZodString;  // add custom validator method
  }
}

// Interface merging — interfaces with the same name merge
interface Theme {
  colors: { primary: string };
}
interface Theme {
  fonts: { body: string };   // merged with above
}
// Theme now has both colors and fonts
Gotcha: Augmentation files must contain at least one import or export statement (even export {}) to be treated as modules. Without this, declarations become global and may conflict.

Branded & Nominal Types

TypeScript uses structural typing by default, so two identical shapes are interchangeable. Branded types simulate nominal typing to prevent accidental misuse. TS Only

// Brand with unique symbol — strongest approach
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };

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

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const userId = "usr_123" as UserId;
const orderId = "ord_456" as OrderId;

getUser(userId);     // OK
getUser(orderId);    // Error — OrderId is not assignable to UserId
getUser("raw_str");  // Error — string is not assignable to UserId

// Constructor functions for runtime validation
function createUserId(raw: string): UserId {
  if (!raw.startsWith("usr_")) throw new Error("Invalid user ID");
  return raw as UserId;
}

// Brand with intersection — lighter approach
type Meters = number & { readonly __unit: "meters" };
type Seconds = number & { readonly __unit: "seconds" };

function speed(distance: Meters, time: Seconds): number {
  return distance / time;  // arithmetic still works
}

const d = 100 as Meters;
const t = 10 as Seconds;
speed(d, t);   // OK
speed(t, d);   // Error — units swapped

Explicit Resource Management & Custom Utility Types

The using keyword (TS 5.2+) provides deterministic cleanup via Symbol.dispose, similar to Python's with or C#'s using. TS Only

// using — sync resource management (TS 5.2+)
class DatabaseConnection implements Disposable {
  constructor(private url: string) {
    console.log(`Connected to ${url}`);
  }
  query(sql: string) { /* ... */ }
  [Symbol.dispose]() {
    console.log("Connection closed");  // guaranteed cleanup
  }
}

{
  using db = new DatabaseConnection("postgres://localhost/mydb");
  db.query("SELECT * FROM users");
}  // db[Symbol.dispose]() called automatically here

// await using — async resource management
class TempFile implements AsyncDisposable {
  constructor(public path: string) {}
  async write(data: string) { /* ... */ }
  async [Symbol.asyncDispose]() {
    await fs.unlink(this.path);  // async cleanup
    console.log(`Deleted ${this.path}`);
  }
}

async function processData() {
  await using tmp = new TempFile("/tmp/data.json");
  await tmp.write(JSON.stringify(payload));
  // tmp is deleted when scope exits, even on error
}

// === Custom utility types — build your own ===

// DeepReadonly — recursively freeze nested objects
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function ? T[K] : DeepReadonly<T[K]>
    : T[K];
};

// DeepPartial — make all nested properties optional
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

// PathKeys — extract dot-separated paths from nested object
type PathKeys<T, Prefix extends string = ""> = T extends object
  ? { [K in keyof T & string]:
      | `${Prefix}${K}`
      | PathKeys<T[K], `${Prefix}${K}.`>
    }[keyof T & string]
  : never;

interface Config {
  db: { host: string; port: number };
  cache: { ttl: number };
}
type ConfigPaths = PathKeys<Config>;
// "db" | "db.host" | "db.port" | "cache" | "cache.ttl"

// Exhaustive type — ensure switch covers all cases
type Exhaustive<T extends never> = T;

type Shape = "circle" | "square" | "triangle";
function area(s: Shape): number {
  switch (s) {
    case "circle": return Math.PI * 10 ** 2;
    case "square": return 10 * 10;
    case "triangle": return (10 * 8) / 2;
    default: return s satisfies never;  // error if a case is missing
  }
}
Pro tip: Use satisfies never in the default branch of a switch/if chain. If you add a new variant to the union and forget to handle it, TypeScript will flag the default as an error at compile time.
11

Testing & Quality

Vitest Setup & Basics

Vitest is a Vite-native test runner with Jest-compatible APIs, native TypeScript support, and fast HMR-based watch mode. TS + JS

// Install
// npm i -D vitest

// vitest.config.ts — project configuration
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,          // use describe/it/expect without imports
    environment: "node",    // or "jsdom", "happy-dom"
    include: ["src/**/*.test.{ts,js}"],
    coverage: {
      provider: "v8",       // or "istanbul"
      reporter: ["text", "html", "lcov"],
      thresholds: { lines: 80, branches: 75, functions: 80 },
    },
  },
});

// src/math.ts
export function add(a: number, b: number): number { return a + b; }
export function divide(a: number, b: number): number {
  if (b === 0) throw new Error("Division by zero");
  return a / b;
}

// src/math.test.ts — test file
import { describe, it, expect } from "vitest";
import { add, divide } from "./math";

describe("math utilities", () => {
  it("adds two numbers", () => {
    expect(add(2, 3)).toBe(5);
    expect(add(-1, 1)).toBe(0);
  });

  it("divides two numbers", () => {
    expect(divide(10, 2)).toBe(5);
    expect(divide(7, 2)).toBeCloseTo(3.5);
  });

  it("throws on division by zero", () => {
    expect(() => divide(1, 0)).toThrow("Division by zero");
  });

  it.each([
    [1, 2, 3],
    [0, 0, 0],
    [-5, 5, 0],
  ])("add(%i, %i) = %i", (a, b, expected) => {
    expect(add(a, b)).toBe(expected);
  });
});

Testing Async Code

Strategies for testing promises, timers, and network requests without hitting real services. TS + JS

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";

// Testing promises — just await the result
async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`User ${id} not found`);
  return res.json();
}

describe("fetchUser", () => {
  beforeEach(() => {
    // Mock global fetch
    vi.stubGlobal("fetch", vi.fn());
  });
  afterEach(() => vi.restoreAllMocks());

  it("returns user data", async () => {
    const mockUser = { id: "1", name: "Ada" };
    vi.mocked(fetch).mockResolvedValue(
      new Response(JSON.stringify(mockUser), { status: 200 })
    );

    const user = await fetchUser("1");
    expect(user).toEqual(mockUser);
    expect(fetch).toHaveBeenCalledWith("/api/users/1");
  });

  it("throws on 404", async () => {
    vi.mocked(fetch).mockResolvedValue(
      new Response(null, { status: 404 })
    );
    await expect(fetchUser("999")).rejects.toThrow("User 999 not found");
  });
});

// Fake timers — control setTimeout, setInterval, Date
describe("debounce", () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it("delays execution", () => {
    const fn = vi.fn();
    const debounced = debounce(fn, 300);

    debounced();
    debounced();
    debounced();

    expect(fn).not.toHaveBeenCalled();
    vi.advanceTimersByTime(300);
    expect(fn).toHaveBeenCalledOnce();
  });

  it("works with Date.now()", () => {
    vi.setSystemTime(new Date("2025-01-15"));
    expect(new Date().getFullYear()).toBe(2025);
  });
});

Mocking Patterns

Replace dependencies with controlled substitutes for isolated, deterministic testing. TS + JS

import { describe, it, expect, vi } from "vitest";

// vi.fn() — create a standalone mock function
const callback = vi.fn();
callback("hello");
expect(callback).toHaveBeenCalledWith("hello");
expect(callback).toHaveBeenCalledTimes(1);

// Mock with implementation
const rand = vi.fn(() => 0.42);
expect(rand()).toBe(0.42);

// Mock return sequence
const getId = vi.fn()
  .mockReturnValueOnce("id-1")
  .mockReturnValueOnce("id-2")
  .mockReturnValue("id-default");

// vi.mock() — mock entire modules
vi.mock("./database", () => ({
  db: {
    query: vi.fn().mockResolvedValue([{ id: 1 }]),
    close: vi.fn(),
  },
}));

// vi.spyOn() — observe real methods, optionally override
import * as utils from "./utils";

const spy = vi.spyOn(utils, "formatDate");
spy.mockReturnValue("2025-01-01");

expect(utils.formatDate(new Date())).toBe("2025-01-01");
expect(spy).toHaveBeenCalledOnce();

spy.mockRestore();  // restore original implementation

// Mock class instance
vi.mock("./EmailService", () => ({
  EmailService: vi.fn().mockImplementation(() => ({
    send: vi.fn().mockResolvedValue({ delivered: true }),
    verify: vi.fn().mockResolvedValue(true),
  })),
}));
Caution: Over-mocking leads to tests that pass but don't catch real bugs. Mock at boundaries (network, filesystem, clock) and prefer real implementations for pure logic.

Type Testing & Code Coverage

Verify that your types behave correctly at compile time, and enforce minimum test coverage thresholds. TS Only

// Install: npm i -D expect-type
import { expectTypeOf } from "expect-type";
import { add, fetchUser } from "./math";

// Test return types
expectTypeOf(add).returns.toBeNumber();
expectTypeOf(fetchUser).returns.resolves.toMatchTypeOf<{ id: string }>();

// Test parameter types
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>();

// Test that a type IS an error (should not compile)
type OnlyStrings<T> = T extends string ? T : never;
expectTypeOf<OnlyStrings<number>>().toBeNever();

// Test assignability
expectTypeOf<{ a: 1 }>().toMatchTypeOf<{ a: number }>();
expectTypeOf<string>().not.toEqualTypeOf<number>();

// Test generic constraints
interface Repository<T extends { id: string }> {
  find(id: string): T | null;
}
expectTypeOf<Repository<{ id: string; name: string }>>().toBeObject();

// === Code Coverage ===
// vitest.config.ts thresholds
// coverage: {
//   provider: "v8",
//   thresholds: {
//     lines: 80,
//     branches: 75,
//     functions: 80,
//     statements: 80,
//   },
// }

// Run coverage:
// npx vitest run --coverage
// npx vitest run --coverage.provider=istanbul  # alt provider

// Per-file thresholds (vitest 1.4+)
// coverage: {
//   thresholds: {
//     "src/critical/**": { lines: 95, branches: 90 },
//     "src/utils/**":    { lines: 70 },
//   }
// }
Note: Type tests run at compile time, not runtime. They ensure your type definitions and generics behave as intended without executing any code. Combine with runtime tests for full confidence.

Testing DOM & Components

Test browser-like environments using jsdom or happy-dom, simulating user events and verifying rendered output. TS + JS

// vitest.config.ts — use jsdom or happy-dom
// test: { environment: "happy-dom" }  // faster, less complete
// test: { environment: "jsdom" }       // more compatible

import { describe, it, expect, beforeEach } from "vitest";

// Testing DOM manipulation
function createCounter(container: HTMLElement) {
  let count = 0;
  container.innerHTML = `
    <span class="count">0</span>
    <button class="inc">+</button>
    <button class="dec">-</button>
  `;
  container.querySelector(".inc")!.addEventListener("click", () => {
    count++;
    container.querySelector(".count")!.textContent = String(count);
  });
  container.querySelector(".dec")!.addEventListener("click", () => {
    count--;
    container.querySelector(".count")!.textContent = String(count);
  });
}

describe("createCounter", () => {
  let container: HTMLDivElement;

  beforeEach(() => {
    container = document.createElement("div");
    document.body.appendChild(container);
    createCounter(container);
  });

  it("starts at 0", () => {
    expect(container.querySelector(".count")!.textContent).toBe("0");
  });

  it("increments on click", () => {
    container.querySelector<HTMLButtonElement>(".inc")!.click();
    container.querySelector<HTMLButtonElement>(".inc")!.click();
    expect(container.querySelector(".count")!.textContent).toBe("2");
  });

  it("decrements on click", () => {
    container.querySelector<HTMLButtonElement>(".dec")!.click();
    expect(container.querySelector(".count")!.textContent).toBe("-1");
  });

  it("responds to keyboard events", () => {
    const event = new KeyboardEvent("keydown", { key: "ArrowUp" });
    document.dispatchEvent(event);
    // test your keyboard handler logic
  });
});
Performance: happy-dom is 2-10x faster than jsdom for most tests. Use jsdom only when you need full browser compatibility (e.g., canvas, IntersectionObserver polyfills). Set environment per-file with // @vitest-environment happy-dom at the top.
12

Common Gotchas & Tips

this Binding Rules

The value of this in JavaScript depends on how a function is called, not where it's defined. Four rules determine binding. TS + JS

// Rule 1: Default binding — standalone call → undefined (strict) or global
function showThis() { console.log(this); }
showThis();  // undefined in strict mode, window/globalThis in sloppy

// Rule 2: Implicit binding — called as method → the object
const obj = {
  name: "Ada",
  greet() { console.log(this.name); },
};
obj.greet();          // "Ada"
const fn = obj.greet;
fn();                 // undefined — implicit binding lost!

// Rule 3: Explicit binding — call/apply/bind
fn.call(obj);         // "Ada"
fn.apply(obj);        // "Ada"
const bound = fn.bind(obj);
bound();              // "Ada"

// Rule 4: new binding — constructor creates fresh object
function Person(name: string) {
  this.name = name;   // 'this' = newly created object
}

// Arrow functions — lexical 'this', ignores all 4 rules
class Timer {
  seconds = 0;

  // BUG: regular function loses 'this' in callback
  startBroken() {
    setInterval(function () {
      this.seconds++;  // 'this' is undefined or window!
    }, 1000);
  }

  // FIX: arrow function captures 'this' from enclosing scope
  start() {
    setInterval(() => {
      this.seconds++;  // 'this' is the Timer instance
    }, 1000);
  }
}

Equality, Floating Point & typeof Quirks

JavaScript's type coercion creates surprising behaviors. Know when == coerces and when === still isn't enough. TS + JS

// == vs === — loose equality coerces types
0 == ""          // true  (both coerce to 0)
0 == "0"         // true  (string → number)
"" == "0"        // false (no coercion path)
false == "0"     // true  (both → 0)
null == undefined // true  (special case)
null == 0        // false (null only == undefined)

// Always use === except for null checks
if (value == null) { /* catches both null and undefined */ }

// Object.is() — strictest comparison
Object.is(NaN, NaN);   // true  (unlike ===)
Object.is(0, -0);      // false (unlike ===)
NaN === NaN;            // false — the classic gotcha
0 === -0;               // true  — hides sign

// Floating point — IEEE 754 strikes again
0.1 + 0.2 === 0.3;              // false!
0.1 + 0.2;                       // 0.30000000000000004

// Solutions:
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON;   // true
+(0.1 + 0.2).toFixed(10) === 0.3;              // true
// For money: use integers (cents) or a decimal library

// typeof quirks
typeof null        // "object"   — 25-year-old bug, never fixed
typeof NaN         // "number"   — NaN is technically a number
typeof []          // "object"   — arrays are objects
typeof function(){} // "function" — the only callable check
typeof undeclared  // "undefined" — no ReferenceError (unlike accessing)

// Better type checks
Array.isArray([]);                      // true
Number.isNaN(NaN);                      // true (not global isNaN!)
Number.isFinite(42);                    // true
value === null;                         // explicit null check
value instanceof Map;                   // prototype chain check
Object.prototype.toString.call(value);  // "[object Type]"

Hoisting, Closures & Array Gotchas

Classic JavaScript traps involving variable hoisting, closure over loop variables, and mutating array methods. TS + JS

// var hoisting — declaration hoisted, assignment is not
console.log(x);  // undefined (not ReferenceError!)
var x = 5;
// Equivalent to:
// var x;
// console.log(x);  // undefined
// x = 5;

// let/const — temporal dead zone (TDZ)
console.log(y);  // ReferenceError: Cannot access 'y' before init
let y = 5;

// Function hoisting — entire function is hoisted
greet();  // works!
function greet() { console.log("Hi"); }

// Function expression — NOT hoisted
hello();  // TypeError: hello is not a function
var hello = function() { console.log("Hello"); };

// Closure gotcha in loops — var captures shared variable
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 — all closures share the same 'i'

// Fix 1: use let (block-scoped, new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2

// Fix 2: IIFE captures value (pre-ES6 pattern)
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 100))(i);
}

// Array methods that MUTATE (modify in place)
const arr = [3, 1, 2];
arr.sort();     // [1, 2, 3] — mutates!
arr.reverse();  // [3, 2, 1] — mutates!
arr.splice(1, 1); // removes index 1 — mutates!

// Non-mutating alternatives (ES2023+)
const sorted = arr.toSorted();    // new sorted array
const reversed = arr.toReversed(); // new reversed array
const without = arr.toSpliced(1, 1); // new array without index 1

// .with() — immutable index replacement
const updated = arr.with(0, 99);  // [99, 2, 1] — new array
Watch out: Array.sort() without a comparator converts elements to strings: [10, 9, 80].sort() yields [10, 80, 9]. Always pass a comparator: .sort((a, b) => a - b).

TypeScript Strict Mode Flags

The strict flag enables a family of checks. Here's what each one catches and why you want them all on. TS Only

// tsconfig.json — "strict": true enables ALL of these:
{
  "compilerOptions": {
    "strict": true,
    // Equivalent to enabling each individually:

    // strictNullChecks — null/undefined are not assignable to other types
    // Without: string can be null at runtime → crashes
    // With:    must explicitly handle string | null
    let name: string;
    name = null;  // Error with strictNullChecks

    // strictFunctionTypes — function params checked contravariantly
    // Catches unsafe function assignments in callbacks
    type Handler = (e: MouseEvent) => void;
    const handler: Handler = (e: Event) => {};  // Error

    // strictBindCallApply — type-check bind(), call(), apply()
    function greet(name: string) { return `Hi ${name}`; }
    greet.call(null, 42);  // Error: expected string, got number

    // strictPropertyInitialization — class fields must be initialized
    class User {
      name: string;       // Error: not initialized in constructor
      age: string = "";   // OK: default value
      role!: string;      // OK: definite assignment assertion
    }

    // noImplicitAny — must declare types, no silent 'any'
    function process(data) {}    // Error: 'data' implicitly has 'any'
    function process(data: unknown) {}  // OK

    // noImplicitThis — 'this' must have a known type
    function onClick() {
      console.log(this.value);  // Error: 'this' implicitly has 'any'
    }

    // useUnknownInCatchVariables — catch variable is 'unknown' not 'any'
    try { /* ... */ } catch (e) {
      e.message;  // Error: 'e' is unknown
      if (e instanceof Error) e.message;  // OK after narrowing
    }

    // alwaysStrict — emit "use strict" in every file
  }
}
Recommendation: Always start new projects with "strict": true. For legacy codebases, enable flags one at a time: start with strictNullChecks (catches the most bugs), then noImplicitAny, then the rest.

Pro Tips & Patterns

Power-user techniques for writing safer, more expressive TypeScript. TS Only

// const assertions — narrow literals to their exact value
const config = {
  api: "https://api.example.com",
  retries: 3,
  methods: ["GET", "POST"],
} as const;
// type: { readonly api: "https://..."; readonly retries: 3;
//         readonly methods: readonly ["GET", "POST"] }

// as const satisfies — validate shape AND keep literal types
const routes = {
  home: "/",
  about: "/about",
  user: "/user/:id",
} as const satisfies Record<string, `/${string}`>;
// Validates: all values start with "/"
// Preserves: routes.home is type "/" not string

// Branded IDs for domain safety
type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };

function getPost(userId: UserId, postId: PostId) { /* ... */ }
// Can't accidentally swap userId and postId

// Exhaustive switch — catch unhandled union members
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

type Status = "active" | "inactive" | "pending";
function handle(s: Status) {
  switch (s) {
    case "active": return doActive();
    case "inactive": return doInactive();
    case "pending": return doPending();
    default: assertNever(s);  // compile error if a case is missing
  }
}

// Template literal types — type-safe string patterns
type EventName = `${"click" | "hover" | "focus"}${"Start" | "End"}`;
// "clickStart" | "clickEnd" | "hoverStart" | "hoverEnd" | ...

type Getter<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// { getName: () => string; getAge: () => number; }

// Discriminated unions — the cornerstone of safe TS modeling
type Result<T> =
  | { ok: true; value: T }
  | { ok: false; error: Error };

function handle<T>(result: Result<T>) {
  if (result.ok) {
    console.log(result.value);  // narrowed: { ok: true; value: T }
  } else {
    console.error(result.error); // narrowed: { ok: false; error: Error }
  }
}

// Infer in conditional types — extract nested types
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<number>;            // number

type FnReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type C = FnReturn<(x: number) => string>;  // string
Key insight: as const satisfies (TS 5.0+) is the best of both worlds — compile-time validation that a value matches a type, while preserving the narrowest possible literal types for autocomplete and type safety.