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