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
Information
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.tsThe --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:
1.4 Compiler Basics
| Command | Purpose |
|---|---|
| tsc | Compile using the local tsconfig.json |
| tsc --watch | Recompile automatically on file save |
| tsc --noEmit | Type-check only, without producing JS output |
| tsc --strict | Enable 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
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 property2.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 string2.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
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 shape3.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
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
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
| Feature | interface | type |
|---|---|---|
| Declaration merging | â Yes | â No |
| Union / intersection | â No | â Yes |
| Implementing in a class | â Yes | â Yes |
| Primitives / tuples | â No | â Yes |
Best Practice
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 manually6.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
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 .timestamp7. 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
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 shapes8.3 Narrowing Techniques
- typeof guards for primitives.
- instanceof guards for classes.
- in operator to check property existence.
- Discriminated unions using a shared literal kind field.
- 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
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[]>; // number10.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>; // 510.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 UserId10.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#.
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 Point2D11.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.
| Term | Meaning |
|---|---|
| Covariant | Subtype relationship preserved (return types, array elements) |
| Contravariant | Subtype relationship reversed (function parameters) |
| Bivariant | Allowed 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 â allowed11.4 Assignability Rules
| Rule | Example |
|---|---|
| Excess properties allowed via a variable | Assigning a variable, not a literal, skips excess-property checks |
| any is assignable to/from anything | Bypasses all checks |
| unknown only assignable to unknown/any | Must narrow first |
| never assignable to everything | It'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 variablePART VIII â Utility Types
12. Built-in Utility Types đ ī¸
| Utility | Description |
|---|---|
| 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>>; // Todo13. 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 level13.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.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/node15.3 Migrating a JS Project
- Add allowJs and checkJs to tsconfig.json.
- Rename files incrementally from .js to .ts.
- Fix type errors file by file, starting from leaf modules.
- 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 mergedmerging-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
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
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
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
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+.
| Version | Highlight |
|---|---|
| 5.0 | Decorators, const type parameters, all-caps enum sorting |
| 5.2 | using declarations (explicit resource management) |
| 5.4 | Preserved narrowing in closures created after the last assignment |
| 5.5 | Inferred 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
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 automaticallyInformation
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 âī¸
| Option | Effect |
|---|---|
| verbatimModuleSyntax | Preserves import/export syntax exactly as written; forces explicit type imports |
| isolatedModules | Ensures every file can be transpiled independently (required by esbuild/SWC/Babel) |
| skipLibCheck | Skips type-checking of .d.ts files for faster builds |
| noUncheckedIndexedAccess | Adds undefined to the result of index signature lookups |
| allowImportingTsExtensions | Permits importing .ts files directly (requires noEmit) |
| useDefineForClassFields | Aligns class field emit with the ECMAScript spec |
Tip
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.
| Strategy | Best for |
|---|---|
| Classic | Legacy, rarely used today |
| Node10 | CommonJS Node projects (formerly "Node") |
| Node16 / NodeNext | Node projects mixing ESM and CJS |
| Bundler | Vite, 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.tsCaution
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
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 --build28.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
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-outputTip
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.
| Library | Style |
|---|---|
| Zod | Schema-first, chainable API, type inferred from schema |
| io-ts | Functional, codec-based, built on fp-ts |
| Valibot | Modular, 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
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-dom31.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
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 stepserver-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
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
32.4 ESM vs CommonJS
| Aspect | ESM | CommonJS |
|---|---|---|
| Syntax | import/export | require/module.exports |
| Loading | Static, asynchronous | Dynamic, 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:initsum.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
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 devInformation
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
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 ./distTip
34.5 tsup
Terminal
npx tsup src/index.ts --format cjs,esm --dtsInformation
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.
35.1 Turborepo
turbo.json
{
"pipeline": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"typecheck": { "dependsOn": ["^build"] }
}
}Tip
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 --all35.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
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
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
43.2 OpenAPI Type Generation
Terminal
npx openapi-typescript ./openapi.yaml -o ./src/api-types.tsusage.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 initgraphql-usage.ts
import { useQuery } from "@apollo/client";
import { GetUserDocument, GetUserQuery } from "./generated/graphql";
const { data } = useQuery<GetUserQuery>(GetUserDocument, { variables: { id: "1" } });Tip
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
- Set declaration: true and declarationMap: true so consumers get types plus "go to source" support.
- Ship both ESM and CJS builds via exports conditions (see Section 27.2).
- Mark peer dependencies like react as peerDependencies, not dependencies.
- 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
- Enable strict mode from day one on new projects.
- Prefer unknown over any for values of uncertain shape.
- Model domain states as discriminated unions instead of optional flags.
- Use utility types (Pick, Omit, Partial) instead of duplicating shapes.
- Avoid type assertions (as) unless you are certain of the underlying value â they bypass safety checks.
- Keep .d.ts declaration files in sync with the actual runtime API they describe.
48.2 Common TypeScript Mistakes
- Using any to silence an error instead of fixing the underlying type.
- Overusing type assertions (as) instead of proper narrowing.
- Forgetting that readonly is shallow â nested objects remain mutable.
- Relying on array index access without noUncheckedIndexedAccess, hiding potential undefined values.
- 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
| Area | Recommendation |
|---|---|
| Consistency | Share a base tsconfig.json across all packages via extends |
| Linting | Enforce @typescript-eslint rules in CI, not just locally |
| Builds | Use project references + incremental builds to keep CI fast as the codebase grows |
| Types at boundaries | Validate all external input (API responses, env vars, forms) at runtime, not just statically |
| Governance | Require passing tsc --noEmit as a merge gate, separate from bundler builds |
Summary
Happy typing! đ