What you’ll learn: Decorators, declaration files, strict mode, branded types, and advanced patterns used in enterprise codebases. You don’t need to master all of this β€” recognize when you see it.


Section 1 β€” Decorators

Decorators are a special syntax for annotating or modifying classes, methods, and properties. Heavily used in NestJS, Angular, and class-based frameworks.

Enable in tsconfig.json: "experimentalDecorators": true

// Class decorator
function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

@sealed
class BankAccount {
    balance: number = 0;
}
// Object.seal prevents adding new properties

// Method decorator
function readonly(target: any, key: string, descriptor: PropertyDescriptor) {
    descriptor.writable = false;
    return descriptor;
}

class Fleet {
    @readonly
    describe() {
        return "I cannot be overridden";
    }
}

// Property decorator
function validate(min: number, max: number) {
    return function(target: any, key: string) {
        let value: number;
        Object.defineProperty(target, key, {
            get: () => value,
            set: (newValue: number) => {
                if (newValue < min || newValue > max) {
                    throw new Error(`${key} must be between ${min} and ${max}`);
                }
                value = newValue;
            }
        });
    };
}

class Metric {
    @validate(0, 100)
    percentage: number = 0;
}

const m = new Metric();
m.percentage = 50;   // OK
m.percentage = 150;  // Error! percentage must be between 0 and 100

Decorator factories (decorators with parameters)

function Log(prefix: string) {
    return function(target: any, key: string, descriptor: PropertyDescriptor) {
        const original = descriptor.value;
        descriptor.value = async function(...args: any[]) {
            console.log(`[${prefix}] ${key} called with`, args);
            const result = await original.apply(this, args);
            console.log(`[${prefix}] ${key} returned`, result);
            return result;
        };
        return descriptor;
    };
}

class FleetService {
    @Log("FleetService")
    async getFleet(fleetId: string): Promise<Fleet> {
        return db.get(fleetId);
    }
}

// Now every call to getFleet logs automatically

Section 2 β€” Declaration Files (.d.ts)

Declaration files tell TypeScript about the types in a JavaScript library (or your own JS code):

// myLibrary.d.ts β€” describes what myLibrary.js exports
declare module "my-library" {
    export interface Config {
        host: string;
        port: number;
        debug?: boolean;
    }

    export function connect(config: Config): Promise<Connection>;
    export function disconnect(): void;

    export class Connection {
        query(sql: string): Promise<Row[]>;
        close(): void;
    }

    export interface Row {
        [key: string]: string | number | boolean | null;
    }
}

// After this, TypeScript knows the types:
import { connect, Config } from "my-library";
const config: Config = { host: "localhost", port: 5432 };

@types packages

Most popular npm packages have type definitions maintained by the community:

# Install types for a package
npm install @types/node --save-dev      # Node.js built-in types
npm install @types/express --save-dev   # Express types
npm install @types/jest --save-dev      # Jest types
npm install @types/lodash --save-dev    # Lodash types

# Some packages include their own types (no @types needed)
# Check: does the npm package have a "types" field in its package.json?

Augmenting existing types (declaration merging)

// Add properties to Express Request object
declare namespace Express {
    interface Request {
        user?: { id: string; role: string };  // added by auth middleware
        requestId?: string;                    // added by logging middleware
    }
}

// Now in any route handler:
app.get("/profile", (req, res) => {
    const userId = req.user?.id;   // TypeScript knows this exists
});

Section 3 β€” Strict Mode

Enable strict mode in tsconfig.json β€” it catches more bugs:

{
    "compilerOptions": {
        "strict": true     // enables ALL strict checks below
    }
}

What strict: true enables:

Option What It Catches
strictNullChecks Null/undefined errors β€” must check before using
noImplicitAny Must annotate types when TypeScript can’t infer
strictFunctionTypes Function parameter type compatibility
strictBindCallApply Correct types for bind/call/apply
strictPropertyInitialization Class properties must be initialized in constructor
noImplicitThis this type must be declared
useUnknownInCatchVariables Caught errors are unknown not any
// With strict: true
function greet(name: string | null) {
    // Error without strictNullChecks: name.toUpperCase() β€” might be null!
    if (name !== null) {
        name.toUpperCase();   // OK β€” TypeScript knows it's not null here
    }
}

// Error catching with strict (useUnknownInCatchVariables):
try {
    doSomething();
} catch (error) {
    // error is 'unknown' β€” must check type before using
    if (error instanceof Error) {
        console.log(error.message);   // OK
    }
    console.log(error.message);   // Error! 'error' is of type 'unknown'
}

Section 4 β€” Branded / Nominal Types

TypeScript uses structural typing β€” two types with the same shape are interchangeable. Sometimes you want to distinguish them:

// Problem: these are both 'string' β€” TypeScript allows mixing them
type UserId = string;
type FleetId = string;

function processFleet(id: FleetId): void { /* ... */ }

const userId: UserId = "user-123";
processFleet(userId);   // TypeScript says OK! But this is wrong!

// Solution: branded types
type Brand<T, B> = T & { readonly _brand: B };

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

// Constructor functions that "brand" the value:
const UserId  = (id: string): UserId  => id as UserId;
const FleetId = (id: string): FleetId => id as FleetId;

function processFleet(id: FleetId): void { /* ... */ }

const userId  = UserId("user-123");
const fleetId = FleetId("fleet-456");

processFleet(fleetId);   // OK
processFleet(userId);    // Error! Argument of type 'UserId' is not assignable to 'FleetId'
processFleet("raw-str"); // Error! Not branded

Section 5 β€” Conditional Types

Types that depend on a condition β€” like ternary but for types:

// T extends U ? IfTrue : IfFalse
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false

// NonNullable (this is how the built-in works):
type MyNonNullable<T> = T extends null | undefined ? never : T;

// Flatten array type:
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type A = Flatten<string[]>;   // string
type B = Flatten<number>;     // number (not an array β€” returns as-is)

infer β€” extract part of a type

// Extract the resolved type from a Promise:
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;

type A = Awaited2<Promise<string>>;           // string
type B = Awaited2<Promise<Promise<number>>>;  // number

// Extract the return type of a function:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;                    // string
type R2 = MyReturnType<(x: number) => Promise<Fleet>>;  // Promise<Fleet>

// Extract array element type:
type ElementType<T extends any[]> = T extends (infer E)[] ? E : never;

type E = ElementType<Fleet[]>;   // Fleet

Section 6 β€” Recursive Types

Types that reference themselves:

// JSON type (value that JSON.parse can return):
type JSONValue =
    | string
    | number
    | boolean
    | null
    | JSONValue[]
    | { [key: string]: JSONValue };

const json: JSONValue = {
    name: "EPIC",
    version: 1,
    active: true,
    config: {
        regions: ["NA", "EU"],
        timeout: null
    }
};

// Deeply nested object type:
type NestedObject<T> = T | { [key: string]: NestedObject<T> };

// Tree structure:
type TreeNode<T> = {
    value: T;
    children: TreeNode<T>[];  // recursive β€” children are also TreeNodes
};

const tree: TreeNode<string> = {
    value: "root",
    children: [
        { value: "child1", children: [] },
        { value: "child2", children: [
            { value: "grandchild", children: [] }
        ]}
    ]
};

Section 7 β€” Advanced Patterns in Real Codebases

Type-safe event emitter

type EventMap = {
    "fleet:created": { fleetId: string; region: string };
    "fleet:updated": { fleetId: string; changes: Partial<Fleet> };
    "fleet:deleted": { fleetId: string };
    "error":         { message: string; code: number };
};

class TypedEventEmitter<Events extends Record<string, unknown>> {
    private listeners = new Map<keyof Events, Function[]>();

    on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
        if (!this.listeners.has(event)) this.listeners.set(event, []);
        this.listeners.get(event)!.push(listener);
    }

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

const bus = new TypedEventEmitter<EventMap>();

// TypeScript enforces correct payload type:
bus.on("fleet:created", ({ fleetId, region }) => {
    console.log(`New fleet ${fleetId} in ${region}`);
});

bus.emit("fleet:created", { fleetId: "RIPE-NA", region: "us-east-1" }); // OK
bus.emit("fleet:created", { fleetId: "RIPE-NA", wrongField: "x" });      // Error!

Builder pattern with TypeScript

class FleetBuilder {
    private fleet: Partial<Fleet> = {};

    withId(fleetId: string): this {
        this.fleet.fleetId = fleetId;
        return this;
    }

    withRegion(region: string): this {
        this.fleet.region = region;
        return this;
    }

    withTpm(tpm: number): this {
        this.fleet.tpm = tpm;
        return this;
    }

    build(): Fleet {
        if (!this.fleet.fleetId) throw new Error("fleetId required");
        if (!this.fleet.region)  throw new Error("region required");
        return {
            fleetId: this.fleet.fleetId,
            region:  this.fleet.region,
            tpm:     this.fleet.tpm ?? 0,
            isActive: true,
            createdAt: new Date().toISOString()
        };
    }
}

const fleet = new FleetBuilder()
    .withId("RIPE-NA")
    .withRegion("us-east-1")
    .withTpm(50000)
    .build();

Section 8 β€” Quick Reference

Decorators:
  @ClassName          Class decorator
  @method             Method decorator
  @property           Property decorator
  @param              Parameter decorator
  Enable: "experimentalDecorators": true in tsconfig

Strict Mode:
  "strict": true           Enable all strict checks
  strictNullChecks         Most important β€” enables null/undefined safety
  noImplicitAny            Must annotate when TypeScript can't infer

Branded Types:
  type Brand<T, B> = T & { _brand: B }
  Used to prevent mixing same-primitive types

Conditional Types:
  T extends U ? X : Y      Conditional type
  infer R                  Extract a type from a pattern
  Awaited<T>               Unwrap Promise (built-in uses infer)
  ReturnType<T>            Return type (built-in uses infer)

Declaration Files:
  .d.ts file              Type declarations for JS code
  @types/package          Community-maintained types
  declare module "x"      Declare types for a module