The Complete TypeScript Handbook 📘

TypeScript is a strongly typed superset of JavaScript that compiles down to plain JavaScript. It adds static types, powerful tooling, and compile-time safety on top of the dynamic language you already know. This handbook walks through everything from your first .ts file to advanced type-level programming, framework integration, and enterprise practice.

Information

Every example below is real, runnable TypeScript. Copy any snippet into the TypeScript Playground to experiment live.

Information

How this handbook is organized: content is grouped into 24 parts that build on one another — start at Part I and move sequentially for a complete learning path, or jump to any part as a reference. Sections marked 🆕 cover concepts not in the original outline but essential for a complete picture of the language.

PART I — Getting Started

1. Introduction & Setup 🚀

1.1 Why TypeScript?

JavaScript is dynamically typed, which means many errors only surface at runtime. TypeScript catches these errors at compile time, before your code ever runs, by analyzing the shapes and types of your values.

  • Static type-checking — catch bugs before shipping.
  • Better tooling — autocomplete, refactoring, and inline docs via IntelliSense.
  • Self-documenting code — types describe intent.
  • Gradual adoption — you can add types incrementally to an existing JS codebase.

1.2 Installing TypeScript

Terminal

npm install -D typescript
npx tsc --init
npx tsc index.ts

The --init flag scaffolds a tsconfig.json file, which controls how the compiler (tsc) behaves.

1.3 Project Structure

A typical TypeScript project separates source and compiled output:

my-app
package.json
tsconfig.json
src
index.ts
types.d.ts

1.4 Compiler Basics

CommandPurpose
tscCompile using the local tsconfig.json
tsc --watchRecompile automatically on file save
tsc --noEmitType-check only, without producing JS output
tsc --strictEnable all strict type-checking options

PART II — Type Fundamentals

2. Basic Types 🧱

2.1 Primitives

primitives.ts

let isDone: boolean = false;
let age: number = 42;
let name: string = "Ada";
let big: bigint = 100n;
let sym: symbol = Symbol("id");
let nothing: null = null;
let notDefined: undefined = undefined;

Tip

In modern TypeScript, explicit annotations like : string are often unnecessary for variables initialized immediately — the compiler infers them. See 3. Type Annotations & Inference.

2.2 any, unknown, and never

  • any disables type-checking entirely — use as a last resort.
  • unknown is the type-safe counterpart of any; you must narrow it before use.
  • never represents values that never occur — functions that always throw or infinite loops.

unknown-vs-any.ts

let a: any = 10;
a.foo.bar; // ✅ compiles, đŸ’Ĩ crashes at runtime

let u: unknown = 10;
u.toFixed(); // ❌ Object is of type 'unknown'
if (typeof u === "number") {
  u.toFixed(); // ✅ narrowed to number
}

function fail(msg: string): never {
  throw new Error(msg);
}

2.3 Arrays & Tuples

arrays-tuples.ts

let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// Tuple: fixed length, fixed types per position
let pair: [string, number] = ["age", 30];

// Named tuple members (readability)
let point: [x: number, y: number] = [10, 20];

// Optional and rest elements in tuples
let flexible: [string, number?, ...boolean[]] = ["ok"];

2.4 🆕 Readonly Arrays & Tuples

Prefixing an array or tuple type with readonly (or using ReadonlyArray<T>) prevents mutating methods like push, pop, and index assignment from being called at all.

readonly-arrays.ts

const nums: readonly number[] = [1, 2, 3];
// nums.push(4); // ❌ Property 'push' does not exist on type 'readonly number[]'

const tuple: readonly [string, number] = ["age", 30];
// tuple[0] = "height"; // ❌ Cannot assign to '0' because it is a read-only property

2.5 Object Type

object-type.ts

let user: { name: string; age: number } = { name: "Ada", age: 36 };

2.6 Literal Types

A literal type restricts a value to one specific value rather than a whole category.

literals.ts

let direction: "up" | "down" | "left" | "right";
direction = "up";     // ✅
direction = "north";  // ❌ not assignable

const status = "active" as const; // literal type "active", not string

2.7 🆕 Optional Chaining & Nullish Coalescing

These two JavaScript operators are ubiquitous in TypeScript code for safely working with values that might be null or undefined, and TypeScript narrows through them correctly.

optional-chaining-nullish.ts

interface Profile { address?: { city?: string } }

function getCity(p: Profile): string {
  // Optional chaining: short-circuits to undefined if any link is nullish
  const city = p.address?.city;

  // Nullish coalescing: only falls back on null/undefined (not "" or 0)
  return city ?? "Unknown city";
}

// Optional call
function run(cb?: () => void) {
  cb?.();
}

2.8 🆕 The Non-null Assertion Operator (!)

The postfix ! tells the compiler "trust me, this value is not null or undefined" without any runtime check. Because it bypasses safety entirely, prefer real narrowing (guards, optional chaining) wherever possible.

non-null-assertion.ts

function getElement(id: string): HTMLElement {
  // document.getElementById can return null; ! asserts it won't here
  return document.getElementById(id)!;
}

Caution

A non-null assertion produces no runtime check — if the value is actually null, you get a runtime crash with no compile-time warning. Use sparingly.

3. Type Annotations & Inference 🔍

3.1 How Inference Works

TypeScript infers types from the initial value assigned to a variable, so you don't always need to annotate.

inference.ts

let count = 5;        // inferred: number
let items = [1, 2, 3]; // inferred: number[]
const config = { retries: 3, url: "/api" }; // inferred object shape

3.2 Contextual Typing

contextual.ts

window.onmousedown = function (event) {
  console.log(event.button); // 'event' inferred as MouseEvent
};

3.3 Best Common Type

When an array holds mixed but related values, TypeScript infers a union as the best common type.

best-common-type.ts

let mixed = [1, "two", 3]; // inferred: (string | number)[]

3.4 Literal Widening & Narrowing

widening-narrowing.ts

let a = "hello";        // widened to string
const b = "hello";      // literal type "hello"

let obj = { kind: "circle" }; // obj.kind widened to string
const obj2 = { kind: "circle" } as const; // obj2.kind is "circle"

Tip

Use as const on object or array literals to prevent widening, which is essential for discriminated unions and tuple inference.

3.5 Type Inference Deep Dive

TypeScript resolves inference candidates in a well-defined order: contextual typing, then the "best common supertype" algorithm across multiple candidate sites, with priority given to sites that appear earlier syntactically.

inference-deep-dive.ts

declare function pair<T>(a: T, b: T): T[];
const p = pair(1, "two"); // T inferred as string | number in some contexts, or an error under strict inference

// Inference from generic return position
function wrap<T>(value: T) { return { value }; }
const wrapped = wrap(42); // { value: number }

PART III — Functions

4. Functions 🧩

4.1 Function Types

functions.ts

function add(a: number, b: number): number {
  return a + b;
}

const multiply = (a: number, b: number): number => a * b;

// Function type expression
let operation: (a: number, b: number) => number;
operation = add;

4.2 Optional, Default & Rest Parameters

params.ts

function greet(name: string, greeting?: string): string {
  return `${greeting ?? "Hello"}, ${name}!`;
}

function power(base: number, exponent: number = 2): number {
  return base ** exponent;
}

function sum(...nums: number[]): number {
  return nums.reduce((a, b) => a + b, 0);
}

Warning

Optional parameters (?) must come after all required parameters.

4.3 Overloads

Function overloads let a single function name accept multiple call signatures.

overloads.ts

function makeDate(timestamp: number): Date;
function makeDate(month: number, day: number, year: number): Date;
function makeDate(monthOrTs: number, day?: number, year?: number): Date {
  if (day !== undefined && year !== undefined) {
    return new Date(year, monthOrTs, day);
  }
  return new Date(monthOrTs);
}

4.4 this Parameters

this-param.ts

interface Button {
  label: string;
  onClick(this: Button, event: Event): void;
}

4.5 🆕 Typing Iterators & Generators

Generator functions get their yielded, returned, and injected types modeled by the built-in Generator<Yield, Return, Next> type, and any object implementing [Symbol.iterator] can be typed as Iterable<T>.

iterators-generators.ts

function* range(start: number, end: number): Generator<number> {
  for (let i = start; i < end; i++) yield i;
}

for (const n of range(1, 4)) {
  console.log(n); // n: number
}

class Playlist implements Iterable<string> {
  private tracks: string[] = ["a", "b", "c"];
  [Symbol.iterator](): Iterator<string> {
    return this.tracks[Symbol.iterator]();
  }
}

PART IV — Objects, Classes & Enums

5. Objects, Interfaces & Type Aliases đŸ—ī¸

5.1 Interfaces

interfaces.ts

interface User {
  readonly id: number;
  name: string;
  email?: string; // optional property
}

const u: User = { id: 1, name: "Grace" };

5.2 Type Aliases

type-aliases.ts

type Point = { x: number; y: number };
type ID = string | number;
type Callback = (data: string) => void;

5.3 Interface vs. Type Alias

Featureinterfacetype
Declaration merging✅ Yes❌ No
Union / intersection❌ No✅ Yes
Implementing in a class✅ Yes✅ Yes
Primitives / tuples❌ No✅ Yes

Best Practice

Prefer interface for public object shapes that may be extended; prefer type for unions, tuples, and function types.

5.4 Extending Interfaces

extends.ts

interface Animal { name: string; }
interface Dog extends Animal { breed: string; }

const rex: Dog = { name: "Rex", breed: "Labrador" };

5.5 Index Signatures

index-signature.ts

interface StringMap {
  [key: string]: string;
}

const dict: StringMap = { hello: "world" };

6. Classes & OOP đŸ›ī¸

6.1 Basic Class Syntax

classes.ts

class Person {
  name: string;
  private age: number;
  protected ssn?: string;
  readonly id: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.id = Math.random();
  }

  greet(): string {
    return `Hi, I'm ${this.name}`;
  }
}

6.2 Access Modifiers

  • public — accessible everywhere (default).
  • private — accessible only within the declaring class.
  • protected — accessible within the class and its subclasses.
  • readonly — assignable only at declaration or in the constructor.

6.3 Parameter Properties

param-properties.ts

class Point {
  constructor(public x: number, public y: number) {}
}
// Equivalent to declaring 'x' and 'y' fields and assigning them manually

6.4 Inheritance & Abstract Classes

inheritance.ts

abstract class Shape {
  abstract area(): number;
  describe(): string {
    return `Area: ${this.area()}`;
  }
}

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

Note

abstract classes cannot be instantiated directly — they exist to be extended.

6.5 Implementing Interfaces

implements.ts

interface Flyable { fly(): void; }

class Bird implements Flyable {
  fly() { console.log("Flap flap!"); }
}

6.6 Static Members

static.ts

class Counter {
  static count = 0;
  static increment() { Counter.count++; }
}

6.7 Getters & Setters

accessors.ts

class Temperature {
  private _celsius = 0;
  get fahrenheit(): number { return this._celsius * 9 / 5 + 32; }
  set fahrenheit(f: number) { this._celsius = (f - 32) * 5 / 9; }
}

6.8 🆕 Mixins

A mixin is a function that takes a class constructor and returns a new, extended constructor — TypeScript's recommended pattern for composing reusable behavior across classes without deep inheritance chains.

mixins.ts

type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now();
  };
}

class Note { constructor(public text: string) {} }
const TimestampedNote = Timestamped(Note);
const n = new TimestampedNote("hello"); // has both .text and .timestamp

7. Enums đŸŽšī¸

7.1 Numeric Enums

numeric-enum.ts

enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}

7.2 String Enums

string-enum.ts

enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
}

7.3 const enum

A const enum is fully inlined at compile time, producing zero runtime footprint.

const-enum.ts

const enum Level { Low, Medium, High }
let l = Level.Medium; // compiles to: let l = 1;

Caution

Avoid numeric enums when interoperating with JSON APIs — string enums are safer to serialize and debug.

PART V — Unions, Narrowing & Type Guards

8. Union, Intersection & Narrowing 🔀

8.1 Union Types

union.ts

function printId(id: string | number) {
  console.log(id);
}

8.2 Intersection Types

intersection.ts

type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // must satisfy BOTH shapes

8.3 Narrowing Techniques

  1. typeof guards for primitives.
  2. instanceof guards for classes.
  3. in operator to check property existence.
  4. Discriminated unions using a shared literal kind field.
  5. Custom type predicates (value is Type).

narrowing.ts

function isString(val: unknown): val is string {
  return typeof val === "string";
}

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2;
    case "square": return shape.side ** 2;
  }
}

8.4 Exhaustiveness Checking

exhaustive.ts

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

8.5 Type Guards Deep Dive

Beyond basic narrowing, TypeScript supports user-defined type guards, assertion functions, and discriminated unions to model complex domains safely.

8.6 Advanced Type Guards

advanced-type-guards.ts

class Cat { meow() {} }
class Dog { bark() {} }

function isCat(pet: Cat | Dog): pet is Cat {
  return pet instanceof Cat;
}

// Combining guards with generics
function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}
const nums = [1, null, 2, null, 3].filter(isNotNull); // number[]

8.7 Assertion Functions

assertion-functions.ts

function assertIsNumber(val: unknown): asserts val is number {
  if (typeof val !== "number") throw new Error("Not a number!");
}

assertion-functions-deep.ts

function assert(condition: unknown, message: string): asserts condition {
  if (!condition) throw new Error(message);
}

function assertIsDefined<T>(val: T): asserts val is NonNullable<T> {
  if (val === undefined || val === null) {
    throw new Error("Expected value to be defined");
  }
}

function process(input?: string) {
  assertIsDefined(input);
  input.trim(); // input narrowed to string
}

PART VI — Generics

9. Generics đŸ§Ŧ

9.1 Generic Functions

generic-functions.ts

function identity<T>(arg: T): T {
  return arg;
}

const first = <T,>(arr: T[]): T => arr[0];

9.2 Generic Interfaces & Classes

generic-classes.ts

interface Box<T> {
  value: T;
}

class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

9.3 Generic Constraints

constraints.ts

interface HasLength { length: number; }

function logLength<T extends HasLength>(item: T): T {
  console.log(item.length);
  return item;
}

9.4 Default Type Parameters

default-generics.ts

interface ApiResponse<T = unknown> {
  data: T;
  error?: string;
}

9.5 keyof with Generics

keyof-generic.ts

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

9.6 Advanced Generic Patterns

advanced-generics.ts

// Conditional generic constraint chaining
type ApiResult<T> = T extends { error: infer E } ? { ok: false; error: E } : { ok: true; data: T };

// Generic builder pattern
class QueryBuilder<T extends Record<string, unknown> = {}> {
  private filters: T = {} as T;
  where<K extends string, V>(key: K, value: V): QueryBuilder<T & Record<K, V>> {
    return new QueryBuilder<T & Record<K, V>>();
  }
}

9.7 Generic Factory Patterns

generic-factory.ts

interface Constructable<T> { new (...args: any[]): T; }

function createInstance<T>(ctor: Constructable<T>, ...args: any[]): T {
  return new ctor(...args);
}

class Widget { constructor(public label: string) {} }
const w = createInstance(Widget, "Save Button");

9.8 Recursive Generic Patterns

recursive-generics.ts

type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;
type Deep = Flatten<number[][][]>; // number

// Recursive builder chaining
class Chain<T extends any[] = []> {
  add<U>(_: U): Chain<[...T, U]> { return new Chain(); }
  build(): T { return [] as unknown as T; }
}

Caution

Deeply recursive conditional types can hit the compiler's recursion depth limit (usually 50 levels) — flatten your logic or add a depth counter for very deep structures.

PART VII — Advanced Type System

10. Advanced Types 🧠

10.1 Mapped Types

mapped-types.ts

type Readonly2<T> = { readonly [K in keyof T]: T[K] };
type Partial2<T> = { [K in keyof T]?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };

10.2 Conditional Types

conditional-types.ts

type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>;   // false

// Distributive over unions
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]

10.3 infer Keyword

infer.ts

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
type Result = ReturnTypeOf<() => string>; // string

type ElementType<T> = T extends (infer U)[] ? U : T;
type Elem = ElementType<number[]>; // number

10.4 Template Literal Types

template-literal-types.ts

type Greeting = `Hello, ${string}!`;
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"

10.5 Recursive Types

recursive-types.ts

type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

10.6 Advanced Conditional Types

advanced-conditional.ts

// Nested conditional chains ("type-level switch")
type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

// Conditional type with multiple infer sites
type Swap<T> = T extends [infer A, infer B] ? [B, A] : T;
type S = Swap<[string, number]>; // [number, string]

10.7 Key Remapping in Mapped Types

The as clause inside a mapped type lets you rename, filter, or transform keys while iterating over them.

key-remapping.ts

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 out keys via 'never'
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

10.8 Advanced Template Literal Types

advanced-template-literals.ts

type Route = `/${string}`;
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `${HttpMethod} ${Route}`;

// Extracting parts with infer inside a template literal type
type ExtractId<T> = T extends `user-${infer Id}` ? Id : never;
type Id1 = ExtractId<"user-42">; // "42"

// Splitting a string type
type Split<S extends string, D extends string> =
  S extends `${infer Head}${D}${infer Rest}` ? [Head, ...Split<Rest, D>] : [S];
type Parts = Split<"a.b.c", ".">; // ["a", "b", "c"]

10.9 Type-Level Programming

Type-level programming pushes computation into the type system itself — arithmetic, string manipulation, and even small state machines can be modeled purely with types, evaluated entirely by the compiler.

type-level-programming.ts

type Length<T extends readonly unknown[]> = T["length"];

type BuildTuple<N extends number, T extends unknown[] = []> =
  T["length"] extends N ? T : BuildTuple<N, [...T, unknown]>;

type Add<A extends number, B extends number> =
  [...BuildTuple<A>, ...BuildTuple<B>]["length"];

type Sum = Add<2, 3>; // 5

10.10 Phantom & Branded Types

Branded (or "phantom") types simulate nominal typing in TypeScript's structural system by attaching a unique, unused tag to a base type.

branded-types.ts

type Brand<T, B extends string> = T & { readonly __brand: B };

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

function asUserId(id: string): UserId { return id as UserId; }

function getUser(id: UserId) { /* ... */ }
getUser(asUserId("u1"));      // ✅
// getUser("u1");             // ❌ plain string is not a UserId

10.11 Exact Types

TypeScript has no built-in "exact" object type (one that forbids extra properties structurally), but you can approximate one for a specific shape.

exact-types.ts

type Exact<T, Shape> = T extends Shape
  ? Exclude<keyof T, keyof Shape> extends never ? T : never
  : never;

function createUser<T>(user: Exact<T, { name: string; age: number }>) { /* ... */ }

11. Structural Typing, Variance & Assignability đŸ—ī¸

11.1 Typed Structural Relationships

TypeScript uses structural typing — compatibility is based on shape, not declared inheritance. This differs from nominally-typed languages like Java or C#.

Structural Typing
Shape-based compatibility
Duck typing
"If it walks like a duck..."

11.2 Structural Typing Deep Dive

Two types are compatible if their members are compatible, regardless of the type's declared name — this is why an object literal with extra methods can still satisfy a narrower interface.

structural-deep-dive.ts

interface Point2D { x: number; y: number; }
interface Point3D { x: number; y: number; z: number; }

function print2D(p: Point2D) { console.log(p.x, p.y); }
const p3: Point3D = { x: 1, y: 2, z: 3 };
print2D(p3); // ✅ Point3D structurally satisfies Point2D

11.3 Variance (Covariance, Contravariance, Bivariance)

Variance describes how subtyping of a compound type relates to subtyping of its components. TypeScript's function parameters are bivariant for method syntax but contravariant for function-typed properties under strictFunctionTypes.

TermMeaning
CovariantSubtype relationship preserved (return types, array elements)
ContravariantSubtype relationship reversed (function parameters)
BivariantAllowed in both directions (method shorthand parameters)

variance.ts

type Animal = { name: string };
type Dog = { name: string; breed: string };

let feed: (a: Animal) => void = (a) => {};
let feedDog: (d: Dog) => void = feed; // ✅ contravariant parameter — allowed

11.4 Assignability Rules

RuleExample
Excess properties allowed via a variableAssigning a variable, not a literal, skips excess-property checks
any is assignable to/from anythingBypasses all checks
unknown only assignable to unknown/anyMust narrow first
never assignable to everythingIt's a subtype of every type

11.5 Excess Property Checks

Object literals are checked for extra properties only when assigned directly to a typed location — assigning through a variable disables the check.

excess-property-checks.ts

interface Config { url: string; }

const cfg1: Config = { url: "/api", timeout: 5000 }; // ❌ excess property 'timeout'

const draft = { url: "/api", timeout: 5000 };
const cfg2: Config = draft; // ✅ no excess-property check via variable

PART VIII — Utility Types

12. Built-in Utility Types đŸ› ī¸

UtilityDescription
Partial<T>Makes all properties optional
Required<T>Makes all properties required
Readonly<T>Makes all properties readonly
Record<K, V>Builds an object type with keys K and values V
Pick<T, K>Selects a subset of properties K from T
Omit<T, K>Removes properties K from T
Exclude<T, U>Excludes members of U from union T
Extract<T, U>Extracts members of T assignable to U
NonNullable<T>Removes null and undefined
ReturnType<F>Extracts a function's return type
Parameters<F>Extracts a function's parameter tuple
Awaited<T>Unwraps nested Promise types
InstanceType<C>Extracts the instance type of a class

utility-types.ts

interface Todo { title: string; description: string; done: boolean; }

type TodoPreview = Pick<Todo, "title" | "done">;
type TodoDraft = Partial<Todo>;
type TodoRecord = Record<string, Todo>;
type WithoutDone = Omit<Todo, "done">;

function fetchTodo(): Promise<Todo> { /* ... */ return null as any; }
type FetchedTodo = Awaited<ReturnType<typeof fetchTodo>>; // Todo

13. Custom Utility Types đŸ§Ŧ

Beyond the built-in utility types, many projects define their own type-level helpers for common transformations. The following sections cover the most widely used custom utilities.

13.1 DeepPartial 🌊

deep-partial.ts

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Settings { ui: { theme: string; fontSize: number }; }
type PartialSettings = DeepPartial<Settings>;
// { ui?: { theme?: string; fontSize?: number } }

13.2 DeepReadonly 🔒

deep-readonly.ts

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

const frozen: DeepReadonly<{ a: { b: number } }> = { a: { b: 1 } };
// frozen.a.b = 2; // ❌ readonly at every level

13.3 Mutable 🔓

mutable.ts

type Mutable<T> = { -readonly [K in keyof T]: T[K] };

interface Frozen { readonly id: number; }
type Unfrozen = Mutable<Frozen>; // { id: number }

13.4 Merge đŸ§Ŧ

merge.ts

type Merge<A, B> = Omit<A, keyof B> & B;

interface Base { id: number; name: string; }
interface Override { name: number; }
type Merged = Merge<Base, Override>; // { id: number; name: number }

13.5 Simplify đŸ§ŧ

Simplify flattens intersection types into a single object shape for cleaner hover tooltips and error messages.

simplify.ts

type Simplify<T> = { [K in keyof T]: T[K] } & {};

type Messy = { a: string } & { b: number };
type Clean = Simplify<Messy>; // { a: string; b: number }

13.6 UnionToIntersection 🔀

union-to-intersection.ts

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

type Combined = UnionToIntersection<{ a: string } | { b: number }>;
// { a: string } & { b: number }

13.7 ValueOf 📋

value-of.ts

type ValueOf<T> = T[keyof T];

interface StatusMap { active: "ACTIVE"; inactive: "INACTIVE"; }
type StatusValue = ValueOf<StatusMap>; // "ACTIVE" | "INACTIVE"

PART IX — Modules, Namespaces & Declaration Files

14. Modules & Namespaces đŸ“Ļ

14.1 ES Modules

modules.ts

// math.ts
export function add(a: number, b: number) { return a + b; }
export default class Calculator {}

// main.ts
import Calculator, { add } from "./math";
import * as MathUtils from "./math";

14.2 Type-only Imports

type-only-imports.ts

import type { User } from "./types";
export type { User };

14.3 Namespaces

Note

Namespaces predate ES Modules and are mostly used today for global scripts or organizing types in .d.ts files.

namespaces.ts

namespace Validation {
  export interface StringValidator {
    isValid(s: string): boolean;
  }
}

14.4 Namespace Merging

Multiple namespace declarations with the same name automatically merge their exported members into a single namespace.

namespace-merging.ts

namespace Shapes {
  export class Circle {}
}
namespace Shapes {
  export class Square {}
}
// Shapes now exposes both Circle and Square
const c = new Shapes.Circle();

15. Declaration Files & Working with JavaScript 📄

15.1 Writing .d.ts Files

global.d.ts

declare module "my-untyped-lib" {
  export function doSomething(input: string): number;
}

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

15.2 Using DefinitelyTyped

Terminal

npm install -D @types/lodash @types/node

15.3 Migrating a JS Project

  1. Add allowJs and checkJs to tsconfig.json.
  2. Rename files incrementally from .js to .ts.
  3. Fix type errors file by file, starting from leaf modules.
  4. Enable strict mode once the bulk of the codebase compiles cleanly.

15.4 🆕 Ambient Declarations for Non-JS Assets

Bundlers commonly let you import non-JavaScript assets like CSS Modules, images, or SVGs. TypeScript needs an ambient module declaration to know what type those imports produce.

assets.d.ts

declare module "*.svg" {
  const content: string;
  export default content;
}

declare module "*.module.css" {
  const classes: { readonly [key: string]: string };
  export default classes;
}

16. Declaration Merging & Module Augmentation 🔀

16.1 Declaration Merging Deep Dive

Beyond interfaces, TypeScript merges several other declaration kinds when they share a name, enabling powerful extension patterns for libraries.

merging.ts

interface Window {
  myGlobal: string;
}
interface Window {
  anotherGlobal: number;
}
// Window now has both properties merged

merging-deep-dive.ts

// namespace + class merging
class Album {
  label: Album.AlbumLabel = { name: "Unknown" };
}
namespace Album {
  export interface AlbumLabel { name: string; }
}

// namespace + function merging
function buildLabel(name: string): string {
  return buildLabel.prefix + name;
}
namespace buildLabel {
  export let prefix = "Label: ";
}

Caution

Type aliases (type) never merge — only interface, namespace, class, function, and enum declarations do.

16.2 Module Augmentation

Module augmentation lets you add new members to an existing module's exported types — commonly used to extend third-party libraries.

augment-express.d.ts

import "express";

declare module "express" {
  interface Request {
    user?: { id: string; role: string };
  }
}

Warning

The file must contain at least one top-level import or export to be treated as a module augmentation rather than a global script.

PART X — Async, Errors & Decorators

17. Asynchronous Programming âŗ

17.1 Promises

promises.ts

function fetchUser(id: number): Promise<{ id: number; name: string }> {
  return fetch(`/users/${id}`).then(res => res.json());
}

17.2 Async/Await

async-await.ts

async function loadUser(id: number) {
  try {
    const res = await fetch(`/users/${id}`);
    if (!res.ok) throw new Error("Request failed");
    const user: { id: number; name: string } = await res.json();
    return user;
  } catch (err) {
    console.error(err);
    throw err;
  }
}

17.3 Typing Promise Combinators

promise-combinators.ts

const results: [number, string] = await Promise.all([
  Promise.resolve(1),
  Promise.resolve("two"),
]);

17.4 Advanced Async Patterns

advanced-async.ts

// Typed retry helper
async function retry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  try {
    return await fn();
  } catch (err) {
    if (attempts <= 1) throw err;
    return retry(fn, attempts - 1);
  }
}

// Async generators
async function* paginate<T>(fetchPage: (page: number) => Promise<T[]>) {
  let page = 0;
  while (true) {
    const items = await fetchPage(page++);
    if (items.length === 0) return;
    yield items;
  }
}

18. Error Handling âš ī¸

18.1 Basics

errors.ts

class ValidationError extends Error {
  constructor(message: string, public field: string) {
    super(message);
    this.name = "ValidationError";
  }
}

function validate(age: number) {
  if (age < 0) throw new ValidationError("Age cannot be negative", "age");
}

Important

In catch blocks, the caught value is typed unknown by default (with useUnknownInCatchVariables) — narrow it before accessing properties.

18.2 Typed Error Handling Patterns

typed-errors.ts

abstract class AppError extends Error {
  abstract readonly code: string;
}

class NotFoundError extends AppError {
  readonly code = "NOT_FOUND";
  constructor(public resource: string) { super(`${resource} not found`); }
}

class ValidationAppError extends AppError {
  readonly code = "VALIDATION_ERROR";
  constructor(public fields: string[]) { super("Validation failed"); }
}

function handle(err: AppError) {
  switch (err.code) {
    case "NOT_FOUND": return 404;
    case "VALIDATION_ERROR": return 400;
    default: return 500;
  }
}

19. Decorators đŸŽ¯

19.1 Legacy Decorators

Decorators are functions that can annotate and modify classes, methods, accessors, properties, or parameters. Enable them via experimentalDecorators or use the newer TC39 stage-3 decorators supported natively since TypeScript 5.0.

decorators.ts

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey}`);
    return original.apply(this, args);
  };
}

class Service {
  @log
  fetchData() { /* ... */ }
}

19.2 Modern Decorators (Stage 3)

Since TypeScript 5.0, decorators follow the TC39 stage-3 proposal natively, with a different signature than the legacy experimentalDecorators API.

modern-decorators.ts

function logged(target: Function, context: ClassMethodDecoratorContext) {
  const name = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`Entering ${name}`);
    return target.apply(this, args);
  };
}

class Greeter {
  @logged
  greet(name: string) { return `Hello, ${name}`; }
}

Warning

Modern decorators do not support decorating parameters, and the API for accessing metadata differs from the legacy proposal — audit third-party decorator libraries before upgrading.

PART XI — TypeScript 5.x & Modern Syntax

20. TypeScript Evolution & Ecosystem 🌍

20.1 Common Tooling

  • ESLint with @typescript-eslint for linting.
  • ts-node for running TypeScript directly in Node.
  • vite, esbuild, or swc for fast transpilation in build pipelines.
  • zod or io-ts for runtime validation that mirrors static types.

21. TypeScript 5.x Features 🆕

TypeScript 5.0 and its subsequent minor releases modernized the compiler's internals and added several syntax-level features that make everyday code terser and safer.

  • Native stage-3 decorators (no experimentalDecorators flag needed).
  • The const modifier on type parameters.
  • The satisfies operator.
  • export type * for re-exporting only types.
  • Multiple config file extension via extends arrays.
  • Smaller, faster compiler thanks to internal rewrites in TS 5.5+.
VersionHighlight
5.0Decorators, const type parameters, all-caps enum sorting
5.2using declarations (explicit resource management)
5.4Preserved narrowing in closures created after the last assignment
5.5Inferred type predicates, faster type-checking

22. satisfies Operator đŸŽ¯

The satisfies operator checks that an expression matches a type without widening or changing the expression's inferred type — the best of both as and plain annotations.

satisfies.ts

type Colors = "red" | "green" | "blue";

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies Record<Colors, string | number[]>;

// palette.red is still known as number[], not string | number[]
palette.red.push(10);

Tip

Use satisfies when you want compile-time shape checking but still need the narrowest possible inferred type for later use.

23. Const Type Parameters 🔒

Adding const before a generic type parameter tells the compiler to infer the literal type of the argument instead of widening it.

const-type-params.ts

function tuplify<const T extends readonly unknown[]>(...args: T): T {
  return args;
}

const t1 = tuplify(1, 2, 3);       // readonly [1, 2, 3]
const t2 = tuplify("a", "b");      // readonly ["a", "b"]

24. Explicit Resource Management (using) â™ģī¸

TypeScript 5.2 introduces using and await using declarations, which call a resource's [Symbol.dispose] method automatically when the variable goes out of scope.

using.ts

class FileHandle {
  constructor(private path: string) { console.log("open", path); }
  [Symbol.dispose]() { console.log("close", this.path); }
}

function readConfig() {
  using file = new FileHandle("config.json");
  // ... use file ...
} // file[Symbol.dispose]() is called here automatically

Information

await using works the same way for async cleanup via [Symbol.asyncDispose].

PART XII — Project Configuration & Build Setup

25. The tsconfig.json Deep Dive âš™ī¸

tsconfig.json (strict)

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

tsconfig.json (modules)

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "target": "ES2022",
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}

tsconfig.json (output)

{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Reference

26. Advanced tsconfig.json Options âš™ī¸

OptionEffect
verbatimModuleSyntaxPreserves import/export syntax exactly as written; forces explicit type imports
isolatedModulesEnsures every file can be transpiled independently (required by esbuild/SWC/Babel)
skipLibCheckSkips type-checking of .d.ts files for faster builds
noUncheckedIndexedAccessAdds undefined to the result of index signature lookups
allowImportingTsExtensionsPermits importing .ts files directly (requires noEmit)
useDefineForClassFieldsAligns class field emit with the ECMAScript spec

Tip

Run tsc --showConfig to see the fully resolved configuration, including inherited values from extends.

27. Module Resolution Strategies 🧭

The moduleResolution setting controls how TypeScript locates imported files. Modern projects generally use Bundler or NodeNext rather than the legacy Node strategy.

StrategyBest for
ClassicLegacy, rarely used today
Node10CommonJS Node projects (formerly "Node")
Node16 / NodeNextNode projects mixing ESM and CJS
BundlerVite, esbuild, webpack — mirrors bundler behavior

27.1 Node16 & NodeNext Module Systems

The Node16/NodeNext module settings make TypeScript's resolution match Node's own ESM/CJS interop rules, including requiring explicit file extensions in relative imports.

nodenext.ts

// tsconfig: "module": "NodeNext"
import { helper } from "./util.js"; // note the .js extension, even though the source is util.ts

Caution

Under NodeNext, whether a file is treated as ESM or CJS depends on the nearest package.json's "type" field, not just the file extension.

27.2 Package Exports & Imports

The exports field in package.json defines a package's public API surface and can map different entry points for ESM, CJS, and types.

package.json

{
  "name": "my-lib",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
    "./utils": "./dist/utils.mjs"
  }
}

Best Practice

List the "types" condition first in each exports entry — TypeScript resolves conditions in order.

27.3 Path Mapping (baseUrl & paths)

Path mapping lets you use clean, absolute-style imports instead of long relative paths, resolved purely at compile time (bundlers need their own matching alias config).

tsconfig.json

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@components/*": ["src/components/*"],
      "@utils": ["src/utils/index.ts"]
    }
  }
}

usage.ts

import { Button } from "@components/Button";
import { formatDate } from "@utils";

28. Project References, Incremental Builds & Composite Projects 🔗

28.1 Project References

Project references let you split a large codebase into smaller TypeScript projects that reference one another, each with its own tsconfig.json.

tsconfig.json (root)

{
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" }
  ],
  "files": []
}

packages/ui/tsconfig.json

{
  "compilerOptions": { "composite": true, "outDir": "../../dist/ui" },
  "references": [{ "path": "../core" }]
}

Terminal

tsc --build

28.2 Incremental Compilation

Enabling incremental writes a .tsbuildinfo file that records prior program state, so subsequent compiles only re-check what changed.

tsconfig.json

{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.cache/tsbuildinfo"
  }
}

Information

tsc --build uses incremental compilation automatically across project references, rebuilding only the projects whose dependencies changed.

28.3 Composite Projects

A composite project is one marked with "composite": true, which enables it to be referenced by other projects. It enforces stricter settings to guarantee reliable incremental builds.

  • Requires declaration: true (referencing projects need .d.ts output).
  • All input files must be matched by include — no implicit inclusion.
  • Referenced projects build in dependency order automatically with tsc --build.

PART XIII — Compiler Performance

29. Performance of the Type System âąī¸

Complex conditional and recursive types can slow down the compiler noticeably. Understanding where the type checker spends time helps keep large codebases responsive in the editor.

29.1 Type-Level Optimization

  • Prefer interfaces over deeply nested intersection types — interfaces are cached better internally.
  • Avoid unnecessary infer chains; resolve simpler cases early with direct conditional branches.
  • Memoize repeated generic instantiations by extracting shared type aliases instead of inlining them repeatedly.

29.2 Compiler Performance Diagnostics

Terminal

tsc --extendedDiagnostics
tsc --generateTrace ./trace-output

Tip

--generateTrace produces a Chrome DevTools-compatible trace you can load in chrome://tracing to see exactly which files and types are slow to check.

29.3 Large Union Optimization

Very large union types (hundreds or thousands of members) can slow assignability checks quadratically. Prefer a single object type keyed by a discriminant plus keyof, or split validation into smaller unions where possible.

large-union.ts

// Slow to check against repeatedly:
type Country = "US" | "CA" | "MX" /* ...hundreds more... */;

// Often faster: derive the union from a single source of truth
const COUNTRIES = ["US", "CA", "MX"] as const;
type Country2 = (typeof COUNTRIES)[number];

PART XIV — Runtime Validation

30. Runtime Type Validation ✅

Static types disappear at compile time — validating data coming from the network, forms, or files requires a runtime validation library that can also generate matching static types.

LibraryStyle
ZodSchema-first, chainable API, type inferred from schema
io-tsFunctional, codec-based, built on fp-ts
ValibotModular, tree-shakeable, function-based schemas

30.1 Zod Integration

zod-example.ts

import { z } from "zod";

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

function parseUser(data: unknown): User {
  return UserSchema.parse(data); // throws on invalid data
}

30.2 io-ts

io-ts-example.ts

import * as t from "io-ts";
import { isRight } from "fp-ts/Either";

const User = t.type({ id: t.number, name: t.string });
type User = t.TypeOf<typeof User>;

const result = User.decode({ id: 1, name: "Ada" });
if (isRight(result)) {
  console.log(result.right.name);
}

30.3 Valibot

valibot-example.ts

import * as v from "valibot";

const UserSchema = v.object({
  id: v.number(),
  name: v.string(),
});

type User = v.InferOutput<typeof UserSchema>;
const user = v.parse(UserSchema, { id: 1, name: "Ada" });

Tip

Valibot's function-based schemas tree-shake better than class-based validators, making it a good fit for size-sensitive frontend bundles.

PART XV — React with TypeScript

31. React with TypeScript âš›ī¸

React and TypeScript pair naturally: component props, state, and events all benefit from static typing, and the @types/react package ships comprehensive typings for the DOM and JSX.

Terminal

npm install react react-dom
npm install -D typescript @types/react @types/react-dom

31.1 JSX & TSX

.tsx files allow JSX syntax inside TypeScript. The jsx compiler option controls how JSX is transformed.

tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "lib": ["DOM", "ES2022"]
  }
}

Note

"react-jsx" uses the automatic JSX runtime, so you no longer need import React from "react" in every file.

31.2 Typing React Components

components.tsx

interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
}

function Button({ label, onClick, variant = "primary" }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{label}</button>;
}

// Components with children
interface CardProps { children: React.ReactNode; }
const Card = ({ children }: CardProps) => <div className="card">{children}</div>;

31.3 Hooks with TypeScript

hooks.tsx

import { useState, useEffect, useRef } from "react";

function useCounter(initial: number = 0) {
  const [count, setCount] = useState<number>(initial);
  const increment = () => setCount((c) => c + 1);
  return { count, increment };
}

function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  useEffect(() => {
    fetch(url).then((r) => r.json()).then(setData);
  }, [url]);
  return data;
}

const inputRef = useRef<HTMLInputElement>(null);

31.4 Context API Typing

context.tsx

interface Theme { mode: "light" | "dark"; toggle: () => void; }

const ThemeContext = React.createContext<Theme | undefined>(undefined);

function useTheme(): Theme {
  const ctx = React.useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
  return ctx;
}

31.5 forwardRef& Generics

forward-ref.tsx

interface InputProps { placeholder?: string; }

const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => (
  <input ref={ref} placeholder={props.placeholder} />
));

// Generic forwardRef requires a small cast helper
function fixedForwardRef<T, P = {}>(
  render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
) {
  return React.forwardRef(render) as (
    props: P & React.RefAttributes<T>
  ) => React.ReactElement | null;
}

PART XVI — Node.js & Backend

32. Node.js with TypeScript 🟩

Terminal

npm install -D typescript @types/node tsx
npx tsx src/index.ts   # run TS directly without a build step

server-basics.ts

import { createServer } from "node:http";

const server = createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ ok: true }));
});

server.listen(3000);

32.1 Express + TypeScript

express-app.ts

import express, { Request, Response, NextFunction } from "express";

const app = express();
app.use(express.json());

interface CreateUserBody { name: string; email: string; }

app.post("/users", (req: Request<{}, {}, CreateUserBody>, res: Response) => {
  const { name, email } = req.body;
  res.status(201).json({ name, email });
});

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  res.status(500).json({ error: err.message });
});

32.2 Fastify + TypeScript

fastify-app.ts

import Fastify from "fastify";

const app = Fastify();

interface Ping { pong: boolean; }

app.get<{ Reply: Ping }>("/ping", async (request, reply) => {
  return { pong: true };
});

app.listen({ port: 3000 });

Tip

Fastify's generic route typing (Fastify<{ Body, Querystring, Reply }>) gives you fully typed request and reply objects per-route without extra middleware.

32.3 Environment Variable Typing

env.d.ts

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      PORT?: string;
      NODE_ENV: "development" | "production" | "test";
    }
  }
}
export {};

Warning

This declaration only documents the shape — it doesn't validate that variables exist at runtime. Pair it with a runtime check (e.g. a Zod schema) at startup.

32.4 ESM vs CommonJS

AspectESMCommonJS
Syntaximport/exportrequire/module.exports
LoadingStatic, asynchronousDynamic, synchronous
Top-level await✅ Supported❌ Not supported
File extension (Node).mjs or "type": "module".cjs or default

PART XVII — Testing

33. TypeScript Testing đŸ§Ē

Testing TypeScript involves two layers: type-checking your test code itself, and choosing a test runner that can execute .ts files quickly, usually via a transpile-only mode.

33.1 Jest with TypeScript

Terminal

npm install -D jest ts-jest @types/jest
npx ts-jest config:init

sum.test.ts

import { sum } from "./sum";

describe("sum", () => {
  it("adds two numbers", () => {
    expect(sum(2, 3)).toBe(5);
  });
});

33.2 Vitest

sum.test.ts (vitest)

import { describe, it, expect } from "vitest";
import { sum } from "./sum";

describe("sum", () => {
  it("adds two numbers", () => {
    expect(sum(2, 3)).toBe(5);
  });
});

Tip

Vitest reuses your Vite config and transpiles TypeScript with esbuild, making it significantly faster than ts-jest for most projects.

33.3 Mock Typing

mocking.ts

import { vi, type Mock } from "vitest";
import { fetchUser } from "./api";

vi.mock("./api");

const mockedFetchUser = fetchUser as Mock<typeof fetchUser>;
mockedFetchUser.mockResolvedValue({ id: 1, name: "Ada" });

PART XVIII — Build Tools

34. Build Tools đŸ—ī¸

Modern TypeScript projects rarely call tsc directly for application bundling — instead, a fast bundler transpiles TypeScript (stripping types without checking them) while tsc --noEmit runs separately for type-checking.

34.1 Vite

Terminal

npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev

Information

Vite uses esbuild in development for near-instant startup and Rollup for optimized production bundles.

34.2 esbuild

build.mjs

import * as esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/index.ts"],
  bundle: true,
  outfile: "dist/bundle.js",
  target: "es2020",
});

Caution

esbuild strips types without checking them — always run tsc --noEmit alongside it in CI to catch type errors.

34.3 SWC

.swcrc

{
  "jsc": {
    "parser": { "syntax": "typescript", "tsx": true },
    "target": "es2022"
  },
  "module": { "type": "es6" }
}

34.4 Bun

Terminal

bun init
bun run index.ts   # runs TypeScript directly, no config needed
bun build ./index.ts --outdir ./dist

Tip

Bun's runtime transpiles TypeScript on the fly (types are stripped, not checked) — it's ideal for scripts and fast local development, but still pair it with tsc --noEmit for real type safety.

34.5 tsup

Terminal

npx tsup src/index.ts --format cjs,esm --dts

Information

tsup is a thin wrapper around esbuild tuned for building npm libraries — it emits both CJS/ESM bundles and .d.ts declaration files in one command.

34.6 Rollup

rollup.config.mjs

import typescript from "@rollup/plugin-typescript";

export default {
  input: "src/index.ts",
  output: { dir: "dist", format: "esm" },
  plugins: [typescript()],
};

PART XIX — Monorepos

35. Monorepos 📁

A TypeScript monorepo hosts multiple packages in one repository, sharing dependencies and tooling while keeping each package independently versioned and typed.

my-monorepo
package.json
tsconfig.base.json
packages

35.1 Turborepo

turbo.json

{
  "pipeline": {
    "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
    "typecheck": { "dependsOn": ["^build"] }
  }
}

Tip

Turborepo caches task outputs, so re-running turbo run typecheck across an unchanged package is nearly instant.

35.2 Nx

Terminal

npx create-nx-workspace@latest my-workspace --preset=ts
nx generate @nx/js:library shared-types
nx run-many --target=typecheck --all

35.3 Shared Types Across Packages

packages/shared-types/src/index.ts

export interface User {
  id: string;
  name: string;
}

export type ApiResponse<T> = { data: T } | { error: string };

Best Practice

Publish shared types as their own package (or workspace project) referenced via project references or a workspace protocol like "workspace:*", so frontend and backend packages stay in sync.

PART XX — Architecture & Design Patterns

36. API Design with TypeScript 🎨

api-design.ts

// Prefer discriminated results over throwing for expected failures
type Result<T, E = string> =
  | { success: true; data: T }
  | { success: false; error: E };

async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { success: false, error: res.statusText };
    return { success: true, data: (await res.json()) as T };
  } catch (e) {
    return { success: false, error: String(e) };
  }
}

37. Domain-Driven Type Modeling đŸ›ī¸

domain-modeling.ts

// Make illegal states unrepresentable
type OrderStatus =
  | { status: "draft" }
  | { status: "placed"; placedAt: Date }
  | { status: "shipped"; placedAt: Date; trackingId: string }
  | { status: "cancelled"; reason: string };

function describe(order: OrderStatus): string {
  switch (order.status) {
    case "draft": return "Not yet placed";
    case "placed": return `Placed on ${order.placedAt.toDateString()}`;
    case "shipped": return `Tracking: ${order.trackingId}`;
    case "cancelled": return `Cancelled: ${order.reason}`;
  }
}

38. Typed Design Patterns 🧩

Classic OOP design patterns translate naturally into TypeScript, gaining compile-time safety on top of their runtime behavior.

38.1 Finite State Machines

fsm.ts

type State = "idle" | "loading" | "success" | "error";
type Event = "FETCH" | "RESOLVE" | "REJECT" | "RESET";

const transitions: Record<State, Partial<Record<Event, State>>> = {
  idle: { FETCH: "loading" },
  loading: { RESOLVE: "success", REJECT: "error" },
  success: { RESET: "idle" },
  error: { RESET: "idle" },
};

function reduce(state: State, event: Event): State {
  return transitions[state][event] ?? state;
}

38.2 Repository Pattern

repository.ts

interface Repository<T, ID> {
  findById(id: ID): Promise<T | null>;
  save(entity: T): Promise<void>;
  delete(id: ID): Promise<void>;
}

class InMemoryUserRepository implements Repository<User, string> {
  private store = new Map<string, User>();
  async findById(id: string) { return this.store.get(id) ?? null; }
  async save(user: User) { this.store.set(user.id, user); }
  async delete(id: string) { this.store.delete(id); }
}

interface User { id: string; name: string; }

38.3 Event-Driven Architecture

event-driven.ts

type EventMap = {
  "user.created": { id: string; name: string };
  "user.deleted": { id: string };
};

class TypedEmitter<Events extends Record<string, unknown>> {
  private handlers: { [K in keyof Events]?: Array<(payload: Events[K]) => void> } = {};

  on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) {
    (this.handlers[event] ??= []).push(handler);
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]) {
    this.handlers[event]?.forEach((h) => h(payload));
  }
}

const bus = new TypedEmitter<EventMap>();
bus.on("user.created", (p) => console.log(p.name));

38.4 Authentication & Authorization Models

auth-models.ts

type Role = "admin" | "editor" | "viewer";

interface Permission {
  resource: string;
  actions: ("read" | "write" | "delete")[];
}

const rolePermissions: Record<Role, Permission[]> = {
  admin: [{ resource: "*", actions: ["read", "write", "delete"] }],
  editor: [{ resource: "posts", actions: ["read", "write"] }],
  viewer: [{ resource: "posts", actions: ["read"] }],
};

function can(role: Role, resource: string, action: string): boolean {
  return rolePermissions[role].some(
    (p) => (p.resource === "*" || p.resource === resource) && p.actions.includes(action as any)
  );
}

39. Functional Programming with TypeScript Îģ

functional.ts

type Option<T> = { some: true; value: T } | { some: false };

function map<T, U>(opt: Option<T>, fn: (v: T) => U): Option<U> {
  return opt.some ? { some: true, value: fn(opt.value) } : opt;
}

// Simple pipe utility
function pipe<A, B, C>(a: A, f1: (a: A) => B, f2: (b: B) => C): C {
  return f2(f1(a));
}

PART XXI — Data & API Integration

40. REST API Client with TypeScript 🌐

api-client.ts

interface Endpoints {
  "GET /users/:id": { params: { id: string }; response: User };
  "POST /users": { body: { name: string }; response: User };
}

interface User { id: string; name: string; }

async function apiGet<K extends "GET /users/:id">(
  path: K,
  params: Endpoints[K] extends { params: infer P } ? P : never
): Promise<Endpoints[K]["response"]> {
  const res = await fetch(`/users/${(params as any).id}`);
  return res.json();
}

41. Form Validation Patterns 📋

form-validation.ts

import { z } from "zod";

const SignupForm = z.object({
  email: z.string().email("Invalid email"),
  password: z.string().min(8, "Must be at least 8 characters"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords must match",
  path: ["confirmPassword"],
});

type SignupFormValues = z.infer<typeof SignupForm>;

42. Database Model Typing đŸ—ƒī¸

db-models.ts

// Separate the persisted shape from the domain shape
interface UserRow { id: number; full_name: string; created_at: string; }
interface User { id: number; fullName: string; createdAt: Date; }

function toDomain(row: UserRow): User {
  return { id: row.id, fullName: row.full_name, createdAt: new Date(row.created_at) };
}

Tip

ORMs like Prisma and Drizzle generate these row-level types automatically from your schema — keep hand-written mapping layers only where you need to reshape data for the domain.

43. Schema-Driven Type Generation 📐

43.1 JSON Schema Integration

json-schema.ts

// json-schema-to-typescript can turn a schema into a .d.ts file
// npx json2ts schema.json > schema.d.ts

interface Product {
  id: string;
  price: number;
  tags?: string[];
}

Information

Tools like json-schema-to-typescript and quicktype generate TypeScript interfaces directly from JSON Schema or sample JSON, keeping types in sync with an external schema source.

43.2 OpenAPI Type Generation

Terminal

npx openapi-typescript ./openapi.yaml -o ./src/api-types.ts

usage.ts

import type { paths } from "./api-types";

type GetUserResponse =
  paths["/users/{id}"]["get"]["responses"]["200"]["content"]["application/json"];

43.3 GraphQL with TypeScript

Terminal

npx graphql-codegen init

graphql-usage.ts

import { useQuery } from "@apollo/client";
import { GetUserDocument, GetUserQuery } from "./generated/graphql";

const { data } = useQuery<GetUserQuery>(GetUserDocument, { variables: { id: "1" } });

Tip

GraphQL Code Generator reads your .graphql operations and schema to emit fully typed hooks and result types, eliminating hand-written response interfaces.

PART XXII — Compiler Internals & Tooling APIs

44. Compiler API 🔧

The TypeScript compiler exposes its parser, checker, and printer as a public API (typescript npm package), letting you build custom lint rules, codemods, or analysis tools.

compiler-api.ts

import ts from "typescript";

const sourceFile = ts.createSourceFile(
  "example.ts",
  "const x: number = 5;",
  ts.ScriptTarget.Latest
);

function visit(node: ts.Node) {
  console.log(ts.SyntaxKind[node.kind]);
  ts.forEachChild(node, visit);
}
visit(sourceFile);

45. Language Service API 🧰

The Language Service API powers editor features like autocomplete, go-to-definition, and quick fixes. It's the same API VS Code's TypeScript extension calls into.

language-service.ts

import ts from "typescript";

const files = new Map<string, string>([["a.ts", "let x = 1;"]]);

const host: ts.LanguageServiceHost = {
  getScriptFileNames: () => [...files.keys()],
  getScriptVersion: () => "0",
  getScriptSnapshot: (fileName) =>
    files.has(fileName) ? ts.ScriptSnapshot.fromString(files.get(fileName)!) : undefined,
  getCurrentDirectory: () => process.cwd(),
  getCompilationSettings: () => ({}),
  getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
  fileExists: ts.sys.fileExists,
  readFile: ts.sys.readFile,
};

const service = ts.createLanguageService(host, ts.createDocumentRegistry());

PART XXIII — Library Authoring & Publishing

46. Creating Custom Utility Libraries 📚

custom-utils.ts

// A small, focused utility library built from patterns covered earlier
export type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
export type ValueOf<T> = T[keyof T];
export type Simplify<T> = { [K in keyof T]: T[K] } & {};

export function isDefined<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

47. Library Authoring & Publishing đŸ“Ļ

47.1 Authoring Checklist

  1. Set declaration: true and declarationMap: true so consumers get types plus "go to source" support.
  2. Ship both ESM and CJS builds via exports conditions (see Section 27.2).
  3. Mark peer dependencies like react as peerDependencies, not dependencies.
  4. Test your published package against a real project using npm pack before release.

47.2 Publishing a Package

package.json

{
  "name": "my-lib",
  "version": "1.0.0",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsup src/index.ts --format cjs,esm --dts",
    "prepublishOnly": "npm run build"
  }
}

47.3 Declaration File Best Practices

  • Avoid any in public .d.ts files — it silently disables checking for every consumer.
  • Use export = only when matching a CommonJS-only runtime export; otherwise prefer named/default ESM exports.
  • Version your declaration files alongside the runtime code — a mismatch causes confusing "works at compile time, fails at runtime" bugs.
  • Run attw (Are The Types Wrong?) in CI to catch ESM/CJS type resolution mismatches before publishing.

PART XXIV — Best Practices, Pitfalls & Enterprise Guidance

48. Best Practices & Common Pitfalls ✅

48.1 Core Best Practices

  1. Enable strict mode from day one on new projects.
  2. Prefer unknown over any for values of uncertain shape.
  3. Model domain states as discriminated unions instead of optional flags.
  4. Use utility types (Pick, Omit, Partial) instead of duplicating shapes.
  5. Avoid type assertions (as) unless you are certain of the underlying value — they bypass safety checks.
  6. Keep .d.ts declaration files in sync with the actual runtime API they describe.

48.2 Common TypeScript Mistakes

  1. Using any to silence an error instead of fixing the underlying type.
  2. Overusing type assertions (as) instead of proper narrowing.
  3. Forgetting that readonly is shallow — nested objects remain mutable.
  4. Relying on array index access without noUncheckedIndexedAccess, hiding potential undefined values.
  5. Mutating a variable typed with a union in a way that widens it unexpectedly.

48.3 Code Organization Best Practices

  • Colocate types with the code that uses them; extract to a shared types.ts only when reused across modules.
  • Group barrel exports (index.ts) sparingly — they can hurt tree-shaking and build performance in large projects.
  • Keep domain types separate from transport/DTO types, converting explicitly at the boundary.

48.4 Enterprise TypeScript Best Practices

AreaRecommendation
ConsistencyShare a base tsconfig.json across all packages via extends
LintingEnforce @typescript-eslint rules in CI, not just locally
BuildsUse project references + incremental builds to keep CI fast as the codebase grows
Types at boundariesValidate all external input (API responses, env vars, forms) at runtime, not just statically
GovernanceRequire passing tsc --noEmit as a merge gate, separate from bundler builds

Summary

This handbook now moves in a single, connected arc: language fundamentals (Parts I–VII) → the type system's deeper mechanics (Parts VIII–XI) → project setup and performance (Parts XII–XIII) → runtime safety and frameworks (Parts XIV–XVII) → tooling and monorepos (Parts XVIII–XIX) → architecture, integration, and internals (Parts XX–XXII) → shipping and maintaining TypeScript at scale (Parts XXIII–XXIV).
Happy typing! 🎉