1. TypeScript Syntax and Basic Types

1.1 Type Annotations and Type Inference

Feature Syntax Description Example
Type Annotation variable: Type Explicitly specify variable type - provides documentation and compile-time checking let age: number = 25;
Type Inference let x = value TypeScript automatically infers type from initial value - reduces verbosity let name = "John";
Function Return Type function(): Type Annotate function return type for clarity and type safety function getAge(): number
Parameter Annotation param: Type Required for function parameters - inference not available function(x: string)
Contextual Typing callback context Type inferred from context where value is used - common in callbacks arr.map(x => x.length)

Example: Type annotations vs inference

// Explicit annotation
let count: number = 10;
let message: string = "Hello";

// Type inference (recommended when obvious)
let inferredCount = 10;        // number
let inferredMsg = "Hello";     // string

// Function with annotations
function add(a: number, b: number): number {
    return a + b;
}

// Contextual typing in callbacks
const numbers = [1, 2, 3];
numbers.map(num => num * 2);  // num inferred as number

1.2 Primitive Types

Type Syntax Values Use Case
string string Text values, template literals, Unicode characters Any textual data - names, messages, HTML
number number Integers, floats, Infinity, NaN, hex, binary, octal All numeric operations - counts, calculations, IDs
boolean boolean true or false Flags, conditions, toggles, binary states
null null Intentional absence of value Explicit "no value" - cleared references
undefined undefined Uninitialized or missing value Default unassigned state, optional params
symbol symbol Unique immutable primitive value Object keys, unique identifiers, constants
bigint ES2020 bigint Arbitrarily large integers beyond Number.MAX_SAFE_INTEGER Large number calculations, cryptography

Example: Primitive type usage

// String types
let username: string = "Alice";
let template: string = `Hello ${username}`;

// Number types
let age: number = 30;
let price: number = 19.99;
let hex: number = 0xf00d;
let binary: number = 0b1010;

// Boolean
let isActive: boolean = true;
let hasPermission: boolean = false;

// Null and undefined
let nothing: null = null;
let notAssigned: undefined = undefined;

// Symbol
let uniqueId: symbol = Symbol("id");

// BigInt
let huge: bigint = 9007199254740991n;
Note: In strict mode (strictNullChecks: true), null and undefined are not assignable to other types unless explicitly allowed via union types.

1.3 Literal Types and Template Literal Types

Type Syntax Description Example
String Literal "value" Exact string value as type - restricts to specific text let dir: "left" | "right"
Numeric Literal 42 Exact number as type - useful for constants and configs let code: 200 | 404
Boolean Literal true | false Specific boolean value - often redundant but useful in unions let success: true
Template Literal Type TS 4.1+ `prefix-${T}` String patterns with type interpolation - generates string combinations `user-${number}`
Uppercase Utility Uppercase<T> Converts string literal to uppercase Uppercase<"hello">
Lowercase Utility Lowercase<T> Converts string literal to lowercase Lowercase<"WORLD">
Capitalize Utility Capitalize<T> Capitalizes first character Capitalize<"name">
Uncapitalize Utility Uncapitalize<T> Lowercases first character Uncapitalize<"Name">

Example: Literal types for type safety

// String literals
type Direction = "north" | "south" | "east" | "west";
let move: Direction = "north";

// Numeric literals
type HttpCode = 200 | 201 | 400 | 404 | 500;
let response: HttpCode = 200;

// Template literal types
type EventName = `on${Capitalize<string>}`;
type UserId = `user-${number}`;
type CSSProperty = `--${string}`;

// Practical template literal usage
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `/${string}`;
type APIRoute = `${HTTPMethod} ${Endpoint}`;

let route: APIRoute = "GET /api/users";

1.4 Array Types and Tuple Types

Type Syntax Description Use Case
Array Syntax 1 Type[] Preferred array notation - clean and readable let nums: number[] = [1, 2, 3]
Array Syntax 2 Array<Type> Generic array notation - useful with complex types Array<string | number>
Readonly Array readonly Type[] Immutable array - prevents modifications readonly number[]
Tuple [Type1, Type2] Fixed-length array with specific types per position [string, number]
Optional Tuple Element [Type, Type?] Tuple element may be undefined - increases flexibility [string, number?]
Rest in Tuple [Type, ...Type[]] Variable-length tuple with typed rest elements [string, ...number[]]
Labeled Tuple TS 4.0+ [name: Type] Named tuple elements for better documentation [x: number, y: number]

Example: Arrays and tuples

// Basic arrays
let numbers: number[] = [1, 2, 3, 4];
let strings: Array<string> = ["a", "b", "c"];

// Multi-dimensional arrays
let matrix: number[][] = [[1, 2], [3, 4]];

// Readonly arrays
let immutable: readonly string[] = ["a", "b"];
// immutable.push("c"); // Error!

// Tuples
let coordinate: [number, number] = [10, 20];
let user: [string, number, boolean] = ["Alice", 30, true];

// Optional tuple elements
let response: [number, string?] = [200];
let fullResponse: [number, string?] = [200, "OK"];

// Rest elements in tuples
let data: [string, ...number[]] = ["label", 1, 2, 3];

// Labeled tuples
type Point = [x: number, y: number];
let point: Point = [10, 20];

1.5 Object Types and Index Signatures

Feature Syntax Description Example
Object Type { prop: Type } Inline object shape definition - quick for simple objects { name: string }
Optional Property prop?: Type Property may be undefined or missing { age?: number }
Readonly Property readonly prop Property cannot be reassigned after initialization readonly id: string
Index Signature [key: Type]: Type Dynamic keys with consistent value type - flexible dictionaries [key: string]: number
String Index [key: string]: T Any string key maps to type T - most common pattern [id: string]: User
Number Index [index: number]: T Numeric indices - array-like objects [index: number]: Item
Symbol Index TS 4.4+ [key: symbol]: T Symbol keys for unique properties [key: symbol]: any
Template Literal Key TS 4.4+ [K: `prop${T}`]: V Pattern-based property keys [K: `data${string}`]: T

Example: Object types and index signatures

// Basic object type
let user: { name: string; age: number } = {
    name: "John",
    age: 30
};

// Optional and readonly properties
let config: {
    host: string;
    port?: number;
    readonly apiKey: string;
} = {
    host: "localhost",
    apiKey: "abc123"
};

// Index signatures
let scores: { [student: string]: number } = {
    "Alice": 95,
    "Bob": 87
};

// Mixed: defined properties + index signature
type Dictionary = {
    count: number;  // known property
    [key: string]: number;  // dynamic keys
};

// Nested objects
let company: {
    name: string;
    address: {
        street: string;
        city: string;
    };
} = {
    name: "TechCorp",
    address: { street: "Main St", city: "NYC" }
};

1.6 Union and Intersection Types

Type Syntax Description Use Case
Union Type Type1 | Type2 Value can be one of several types - logical OR string | number
Intersection Type Type1 & Type2 Value must satisfy all types - logical AND Person & Employee
Discriminated Union type with literal Union with common discriminant property for type narrowing { kind: "a" } | { kind: "b" }
Union with null Type | null Allow null values explicitly - common with strict null checks string | null
Union with undefined Type | undefined Allow undefined values - optional-like behavior number | undefined
Multi-way Union A | B | C | D More than two types in union - common for enums alternative "left" | "right" | "up"

Example: Union types for flexibility

// Basic union
let id: string | number;
id = "ABC123";  // OK
id = 42;        // OK

// Union with null
function find(name: string): User | null {
    return database.find(name) ?? null;
}

// Discriminated union (tagged union)
type Success = { status: "success"; data: any };
type Error = { status: "error"; message: string };
type Result = Success | Error;

function handle(result: Result) {
    if (result.status === "success") {
        console.log(result.data);  // Type narrowed to Success
    } else {
        console.log(result.message); // Type narrowed to Error
    }
}

Example: Intersection types for composition

// Basic intersection
type Person = { name: string; age: number };
type Employee = { employeeId: string; department: string };
type Worker = Person & Employee;

let worker: Worker = {
    name: "John",
    age: 30,
    employeeId: "E123",
    department: "IT"
};

// Mixin pattern with intersections
type Timestamped = { createdAt: Date; updatedAt: Date };
type Versioned = { version: number };
type Document = { id: string; content: string };

type FullDocument = Document & Timestamped & Versioned;

1.7 any, unknown, never, and void Types

Type Behavior Description Use Case
any Disables type checking Escape hatch - allows any operation without type safety Migration, third-party libs, quick prototypes
unknown TS 3.0+ Type-safe any Requires type checking before use - safer than any User input, API responses, dynamic data
never Represents impossible Functions that never return, unreachable code, exhaustive checks Error throwing, infinite loops, type guards
void No return value Function completes but returns nothing meaningful Event handlers, loggers, side-effect functions

any vs unknown

Feature any unknown
Type Safety None Full
Operations All allowed Requires checks
Assignment To/from any type From any, to unknown only
Recommendation Avoid if possible Preferred over any

void vs never

Feature void never
Return Function completes Never completes
Value undefined No value possible
Usage Side effects Errors, unreachable

Example: any vs unknown

// any - no type safety
let data: any = getUserInput();
data.anything();        // No error, dangerous!
data.toUpperCase();     // Runtime error if not string

// unknown - type-safe
let safeData: unknown = getUserInput();
// safeData.anything(); // Error! Must check first

if (typeof safeData === "string") {
    safeData.toUpperCase(); // OK, narrowed to string
}

Example: never and void

// void - function completes but returns nothing
function log(message: string): void {
    console.log(message);
    // implicitly returns undefined
}

// never - function never returns
function throwError(message: string): never {
    throw new Error(message);
    // execution stops here
}

function infiniteLoop(): never {
    while (true) {
        // never exits
    }
}

// never in exhaustive checks
type Shape = "circle" | "square";
function assertNever(x: never): never {
    throw new Error("Unexpected: " + x);
}

function getArea(shape: Shape): number {
    switch (shape) {
        case "circle": return 1;
        case "square": return 2;
        default: return assertNever(shape); // Compile error if not exhaustive
    }
}
Warning: Avoid any whenever possible - it defeats TypeScript's purpose. Use unknown when you need dynamic typing with safety.

2. Advanced Type System Features

2.1 Type Aliases and Interface Declarations

Feature Syntax Description Use Case
Type Alias type Name = Type Creates named reference to any type - unions, intersections, primitives Complex unions, utility compositions
Interface interface Name { } Defines object shape - extendable and mergeable Object contracts, class implementations
Interface Extension extends Interface Inherit properties from parent interfaces Hierarchical type relationships
Type Intersection Type1 & Type2 Combine multiple type aliases Mixins, composed types
Declaration Merging Multiple interface declarations Interfaces with same name automatically merge - not available for types Augmenting libraries, modules

Type vs Interface

Feature Type Interface
Unions
Intersections Use extends
Primitives
Tuples
Merging
Computed Props

Example: Type alias

type ID = string | number;
type Point = [number, number];
type Callback = (data: string) => void;

Example: Interface

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

interface Admin extends User {
    permissions: string[];
}

Example: Type aliases vs interfaces

// Type alias - flexible for any type
type StringOrNumber = string | number;
type Coordinate = [number, number];
type UserID = string;

// Interface - best for object shapes
interface Person {
    name: string;
    age: number;
}

interface Employee extends Person {
    employeeId: string;
    department: string;
}

// Declaration merging (interfaces only)
interface Window {
    customProperty: string;
}
interface Window {
    anotherProperty: number;
}
// Window now has both properties

// Type composition
type WithTimestamp = { timestamp: Date };
type TrackedPerson = Person & WithTimestamp;

2.2 Enum Types and Const Enums

Type Syntax Description Compiled Output
Numeric Enum enum Name { A, B } Auto-incrementing numeric values starting from 0 Object with reverse mapping
String Enum enum { A = "a" } Explicit string values - no auto-increment Object without reverse mapping
Heterogeneous Enum enum { A = 1, B = "b" } Mix of numeric and string values - not recommended Mixed object
Const Enum const enum Name { } Completely inlined at compile time - no runtime object Values inlined, no object
Computed Member A = expression Value computed from expression Evaluated at compile time
Ambient Enum declare enum Name { } Describe existing enum from external source Type-only, no JS output

Example: Enum types

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

// String enum (preferred)
enum Status {
    Pending = "PENDING",
    Active = "ACTIVE",
    Completed = "COMPLETED"
}

// Custom numeric values
enum HttpStatus {
    OK = 200,
    NotFound = 404,
    ServerError = 500
}

// Const enum (performance optimization)
const enum Colors {
    Red = "#FF0000",
    Green = "#00FF00",
    Blue = "#0000FF"
}

// Usage
let status: Status = Status.Active;
let color = Colors.Red;  // Inlined to "#FF0000" at compile time
Note: Prefer string enums or union of string literals over numeric enums for better debugging and runtime safety. Use const enum for zero-runtime overhead.

2.3 Type Assertions

Syntax Form Description Use Case
as Syntax value as Type Preferred modern syntax - works in JSX All scenarios, especially TSX/JSX files
Angle Bracket <Type>value Original syntax - conflicts with JSX Non-JSX files only
Double Assertion x as unknown as T Force assertion through unknown - escape hatch Incompatible types, migrations
Const Assertion TS 3.4+ as const Make literal types readonly and narrow to literal Immutable data, exact types
Non-null Assertion value! Assert value is not null/undefined When you know value exists

Example: Type assertions

// as syntax (preferred)
let input = document.getElementById("input") as HTMLInputElement;
input.value = "Hello";

// Angle bracket syntax (avoid in JSX)
let element = <HTMLElement>document.querySelector(".item");

// Double assertion (escape hatch)
let num = "123" as unknown as number;  // Dangerous!

// Const assertion - deep readonly
let config = {
    host: "localhost",
    port: 3000,
    flags: ["debug", "verbose"]
} as const;
// Type: { readonly host: "localhost"; readonly port: 3000; readonly flags: readonly ["debug", "verbose"] }

// Array as const
let arr = [1, 2, 3] as const;  // Type: readonly [1, 2, 3]

// Non-null assertion
function process(value: string | null) {
    let length = value!.length;  // Assert value is not null
}
Warning: Type assertions bypass type checking - use sparingly. Prefer type guards and narrowing. as const is safe and recommended.

2.4 Type Guards and Narrowing Techniques

Technique Syntax Description Narrows To
typeof Guard typeof x === "type" Check primitive types at runtime string, number, boolean, etc.
instanceof Guard x instanceof Class Check if object is instance of class Class type
in Operator "prop" in obj Check property existence Type with that property
Truthiness Check if (value) { } Filters out null, undefined, 0, "", false Non-nullable type
Equality Check x === value Compare with specific value Literal type
User-defined Guard is Type Custom type predicate function Custom type
Discriminated Union switch (x.kind) Use discriminant property to narrow Specific union member

Example: Built-in type guards

function process(value: string | number) {
    // typeof guard
    if (typeof value === "string") {
        console.log(value.toUpperCase());  // string
    } else {
        console.log(value.toFixed(2));     // number
    }
}

// instanceof guard
function handle(x: Date | string) {
    if (x instanceof Date) {
        console.log(x.getFullYear());  // Date
    } else {
        console.log(x.length);         // string
    }
}

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

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

// Truthiness narrowing
function print(text: string | null) {
    if (text) {
        console.log(text.toUpperCase());  // string
    }
}

Example: User-defined type guards

// Type predicate function
function isString(value: unknown): value is string {
    return typeof value === "string";
}

function process(input: unknown) {
    if (isString(input)) {
        console.log(input.toUpperCase());  // Narrowed to string
    }
}

// Complex type guard
interface Cat { meow: () => void; }
interface Dog { bark: () => void; }

function isCat(animal: Cat | Dog): animal is Cat {
    return (animal as Cat).meow !== undefined;
}

function speak(animal: Cat | Dog) {
    if (isCat(animal)) {
        animal.meow();
    } else {
        animal.bark();
    }
}

2.5 Discriminated Unions and Tagged Unions

Component Description Example
Discriminant Property Common property with literal type - acts as type tag kind: "success" | "error"
Union Members Each type in union has unique discriminant value Success | Error
Exhaustiveness Check Ensure all union cases are handled default: assertNever(x)

Example: Discriminated unions for safe state management

// Define discriminated union
type Result<T> =
    | { status: "loading" }
    | { status: "success"; data: T }
    | { status: "error"; error: Error };

function handle<T>(result: Result<T>) {
    switch (result.status) {
        case "loading":
            console.log("Loading...");
            break;
        case "success":
            console.log(result.data);  // Type: T
            break;
        case "error":
            console.log(result.error.message);  // Type: Error
            break;
    }
}

// Shape discriminated union
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "rectangle"; width: number; height: number }
    | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
        case "triangle":
            return 0.5 * shape.base * shape.height;
    }
}

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

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle": return Math.PI * shape.radius ** 2;
        case "rectangle": return shape.width * shape.height;
        case "triangle": return 0.5 * shape.base * shape.height;
        default: return assertNever(shape);  // Compile error if case missing
    }
}

2.6 Mapped Types and Key Remapping

Pattern Syntax Description Result
Basic Mapped Type { [K in Keys]: Type } Iterate over keys and map to type New object type with mapped properties
Readonly Mapping { readonly [K in Keys] } Make all properties readonly Readonly<T>
Optional Mapping { [K in Keys]?: Type } Make all properties optional Partial<T>
Remove Modifiers -readonly, -? Remove readonly or optional modifiers Required<T>
Key Remapping TS 4.1+ as NewKey Transform key names during mapping Renamed property keys
Filter Keys as K extends X ? K : never Conditionally include/exclude keys Filtered property set

Example: Mapped types

// Basic mapped type
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

type User = { name: string; age: number };
type NullableUser = Nullable<User>;
// Result: { name: string | null; age: number | null }

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

type Required<T> = {
    [K in keyof T]-?: T[K];
};

// Key remapping with template literals
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

// Filter properties by type
type StringKeys<T> = {
    [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type Data = { id: number; name: string; email: string; count: number };
type StringProps = StringKeys<Data>;
// Result: { name: string; email: string }

2.7 Conditional Types and Utility Types

Pattern Syntax Description Use Case
Conditional Type T extends U ? X : Y Type-level if-else - select type based on condition Dynamic type selection
infer Keyword TS 2.8+ infer R Extract type from another type within conditional Return types, array elements
Distributive T extends U when T is union Conditional applied to each union member separately Union transformations
Non-Distributive [T] extends [U] Conditional applied to whole type, not distributed Exact type matching

Example: Conditional types

// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;   // true
type B = IsString<number>;   // false

// Extract return type with infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Func = () => string;
type Result = ReturnType<Func>;  // string

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

// Distributive conditional (union)
type ToArray<T> = T extends any ? T[] : never;
type Arrays = ToArray<string | number>;
// Result: string[] | number[] (distributed)

// Non-distributive (tuple)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type TupleArray = ToArrayNonDist<string | number>;
// Result: (string | number)[] (not distributed)

// Complex conditional - function overloads
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Value1 = UnwrapPromise<Promise<string>>;  // string
type Value2 = UnwrapPromise<number>;           // number

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

Key Takeaways

  • Use interfaces for object shapes and type aliases for unions/intersections
  • Prefer string enums or union literals over numeric enums
  • as const assertions are safe; regular type assertions are dangerous
  • Type guards enable safe type narrowing at runtime
  • Discriminated unions with exhaustiveness checks ensure type safety
  • Mapped types transform object properties systematically
  • Conditional types with infer enable advanced type extraction

3. Generics and Generic Programming

3.1 Generic Functions and Method Signatures

Feature Syntax Description Use Case
Generic Function function<T>(arg: T): T Type parameter allows function to work with multiple types Reusable functions, identity operations
Multiple Type Params <T, U, V> Function can accept multiple generic types Complex transformations, tuple operations
Arrow Function Generic <T>(arg: T) => T Generic arrow function syntax Callbacks, inline functions
Generic Method method<T>(arg: T) Class method with type parameter Flexible class methods
Type Inference Implicit from arguments TypeScript infers generic type from usage - no explicit type needed Clean API usage, reduced verbosity

Example: Generic functions

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

// Usage with explicit type
let output1 = identity<string>("hello");

// Usage with type inference (preferred)
let output2 = identity("hello");  // T inferred as string

// Generic with array
function firstElement<T>(arr: T[]): T | undefined {
    return arr[0];
}

let first = firstElement([1, 2, 3]);  // number | undefined

// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

let p = pair("age", 30);  // [string, number]

// Generic arrow function
const map = <T, U>(arr: T[], fn: (item: T) => U): U[] => {
    return arr.map(fn);
};

// Generic with constraints
function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

Example: Generic methods in classes

class DataStore {
    private data: any[] = [];
    
    // Generic method
    add<T>(item: T): void {
        this.data.push(item);
    }
    
    get<T>(index: number): T {
        return this.data[index];
    }
    
    filter<T>(predicate: (item: T) => boolean): T[] {
        return this.data.filter(predicate);
    }
}

const store = new DataStore();
store.add<string>("hello");
store.add<number>(42);

3.2 Generic Interfaces and Generic Classes

Feature Syntax Description Example
Generic Interface interface Name<T> { } Interface with type parameter - reusable contracts interface Box<T>
Generic Class class Name<T> { } Class with type parameter - typed containers class Stack<T>
Generic Type Alias type Name<T> = ... Type alias with parameter - composable types type Result<T>
Implements Generic implements Interface<T> Class implements generic interface with specific or generic type implements Comparable<T>

Example: Generic interfaces

// Generic interface
interface Container<T> {
    value: T;
    getValue(): T;
    setValue(val: T): void;
}

// Implement with specific type
class StringContainer implements Container<string> {
    constructor(public value: string) {}
    
    getValue(): string {
        return this.value;
    }
    
    setValue(val: string): void {
        this.value = val;
    }
}

// Generic function interface
interface Transformer<T, U> {
    (input: T): U;
}

const toNumber: Transformer<string, number> = (str) => parseInt(str);

// Generic interface with multiple type parameters
interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

Example: Generic classes

// Generic class
class Box<T> {
    private contents: T;
    
    constructor(value: T) {
        this.contents = value;
    }
    
    get(): T {
        return this.contents;
    }
    
    set(value: T): void {
        this.contents = value;
    }
}

const stringBox = new Box<string>("hello");
const numberBox = new Box(42);  // Type inferred

// Generic class with multiple parameters
class Pair<T, U> {
    constructor(
        public first: T,
        public second: U
    ) {}
    
    swap(): Pair<U, T> {
        return new Pair(this.second, this.first);
    }
}

// Generic collection class
class Collection<T> {
    private items: T[] = [];
    
    add(item: T): void {
        this.items.push(item);
    }
    
    get(index: number): T {
        return this.items[index];
    }
    
    findFirst(predicate: (item: T) => boolean): T | undefined {
        return this.items.find(predicate);
    }
}

3.3 Generic Constraints with extends Keyword

Constraint Type Syntax Description Use Case
Basic Constraint T extends Type T must be assignable to Type - restricts generic Ensure minimum type requirements
Object Constraint T extends object T must be object type - excludes primitives Object manipulation functions
Interface Constraint T extends Interface T must implement interface - ensures properties/methods Polymorphic functions
keyof Constraint K extends keyof T K must be property key of T - type-safe property access Property getters/setters
Union Constraint T extends A | B T must be one of specified types Limited type options
Multiple Constraints T extends A & B T must satisfy multiple constraints simultaneously Complex requirements

Example: Generic constraints

// Basic constraint - ensure property exists
interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // OK - length guaranteed
    return arg;
}

logLength("hello");        // OK - string has length
logLength([1, 2, 3]);      // OK - array has length
// logLength(10);          // Error - number has no length

// keyof constraint for type-safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

let person = { name: "Alice", age: 30 };
let name = getProperty(person, "name");    // string
let age = getProperty(person, "age");      // number
// let x = getProperty(person, "invalid"); // Error

// Multiple constraints
interface Named { name: string; }
interface Aged { age: number; }

function describe<T extends Named & Aged>(obj: T): string {
    return `${obj.name} is ${obj.age} years old`;
}

// Constructor constraint
function create<T>(constructor: new () => T): T {
    return new constructor();
}

class Person {
    name = "Unknown";
}

let person = create(Person);  // Person instance

3.4 Default Generic Parameters

Feature Syntax Description Use Case
Default Type <T = DefaultType> Fallback type when generic not provided Optional configuration types
Dependent Default <T, U = T> Default depends on another type parameter Related type pairs
Conditional Default <T = T extends X ? A : B> Default computed from conditional type Smart defaults based on constraints

Example: Default generic parameters

// Basic default
interface Container<T = string> {
    value: T;
}

let c1: Container = { value: "hello" };        // T defaults to string
let c2: Container<number> = { value: 42 };     // T explicitly number

// Multiple parameters with defaults
class Request<TData = any, TError = Error> {
    data?: TData;
    error?: TError;
}

// Use all defaults
let req1 = new Request();                      // Request<any, Error>

// Override first, use second default
let req2 = new Request<string>();              // Request<string, Error>

// Override both
let req3 = new Request<string, CustomError>(); // Request<string, CustomError>

// Dependent default - U defaults to T
function create<T, U = T>(value: T): [T, U] {
    return [value, value as unknown as U];
}

// Default from conditional
type APIResponse<T = never> = T extends never
    ? { status: "idle" }
    : { status: "success"; data: T } | { status: "error"; error: string };

let response1: APIResponse = { status: "idle" };
let response2: APIResponse<string> = { status: "success", data: "hello" };

3.5 Conditional Types with infer Keyword

Pattern Syntax Description Extracts
infer in Conditional T extends Pattern<infer U> Extract type from pattern match Type variable U
Function Return (...args) => infer R Extract function return type Return type
Function Parameters (infer P) => any Extract parameter types Parameter tuple
Array Element (infer U)[] Extract array element type Element type
Promise Value Promise<infer V> Extract promise resolved type Resolved type
Multiple infer infer A, infer B Extract multiple types from pattern Multiple type variables

Example: infer keyword usage

// Extract return type
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Func1 = () => string;
type Return1 = GetReturnType<Func1>;  // string

// Extract parameter types
type GetParameters<T> = T extends (...args: infer P) => any ? P : never;

type Func2 = (a: string, b: number) => void;
type Params = GetParameters<Func2>;  // [string, number]

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

type Numbers = ElementType<number[]>;  // number

// Unwrap Promise
type Unpromise<T> = T extends Promise<infer U> ? U : T;

type Resolved = Unpromise<Promise<string>>;  // string
type NotPromise = Unpromise<number>;         // number

// Deep unwrap nested Promises
type DeepUnpromise<T> = T extends Promise<infer U> 
    ? DeepUnpromise<U> 
    : T;

type Deep = DeepUnpromise<Promise<Promise<number>>>;  // number

// Extract first array element type
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

type FirstType = First<[string, number, boolean]>;  // string

// Extract last array element type
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type LastType = Last<[string, number, boolean]>;  // boolean

// Extract constructor parameter types
type ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;

class MyClass {
    constructor(name: string, age: number) {}
}

type Params2 = ConstructorParams<typeof MyClass>;  // [string, number]

3.6 Variance and Covariance in Generics

Concept Relationship Description Example Context
Covariance T<Child> ⊆ T<Parent> Generic preserves subtype relationship - safe for output positions Return types, readonly arrays
Contravariance T<Parent> ⊆ T<Child> Generic reverses subtype relationship - safe for input positions Function parameters
Invariance No relationship Generic allows no subtype substitution - most restrictive Mutable containers
Bivariance Both directions Generic accepts both super and subtypes - least safe Legacy method signatures

Variance Rules

Position Variance
Return Type Covariant
Parameter Type Contravariant
Readonly Property Covariant
Writable Property Invariant
Method (strict) Contravariant params

Array Variance

Type Variance
Mutable Array Covariant (unsafe)
Readonly Array Covariant (safe)
Tuple Covariant elements

Example: Covariance - return types

class Animal {
    name: string = "";
}

class Dog extends Animal {
    breed: string = "";
}

// Covariance in return types
type Producer<T> = () => T;

let animalProducer: Producer<Animal> = () => new Animal();
let dogProducer: Producer<Dog> = () => new Dog();

// Covariant - Dog producer is subtype of Animal producer
animalProducer = dogProducer;  // OK - Dog extends Animal

// Readonly arrays are covariant (safe)
let animals: readonly Animal[] = [];
let dogs: readonly Dog[] = [new Dog()];

animals = dogs;  // OK - readonly, so safe

Example: Contravariance - parameter types

// Contravariance in function parameters
type Consumer<T> = (arg: T) => void;

let animalConsumer: Consumer<Animal> = (animal: Animal) => {
    console.log(animal.name);
};

let dogConsumer: Consumer<Dog> = (dog: Dog) => {
    console.log(dog.breed);
};

// Contravariant - Animal consumer is subtype of Dog consumer
dogConsumer = animalConsumer;  // OK - can handle Dog since it's an Animal
// animalConsumer = dogConsumer; // Error - cannot handle all Animals

Example: Invariance - mutable containers

// Mutable arrays are technically covariant but unsafe
let animals: Animal[] = [];
let dogs: Dog[] = [new Dog()];

animals = dogs;  // OK in TypeScript (unsound)

// This is why it's unsafe:
animals.push(new Animal());  // Runtime error! Added Animal to Dog[]

// Solution: Use readonly for true covariance
function printAnimals(animals: readonly Animal[]) {
    animals.forEach(a => console.log(a.name));
}

printAnimals(dogs);  // Safe - cannot modify array
Note: TypeScript's array covariance is unsound but pragmatic. Use readonly arrays for type-safe covariance. Enable strictFunctionTypes for better variance checking in function parameters.

Generics Best Practices

  • Use type inference when possible - avoid explicit type arguments
  • Apply constraints to ensure type safety and enable useful operations
  • Provide default types for optional flexibility
  • Use infer for extracting types from complex type patterns
  • Understand variance for safe generic type relationships
  • Prefer readonly for covariant collections

4. Utility Types and Built-in Type Helpers

4.1 Partial<T> and Required<T> for Property Modification

Utility Type Syntax Description Use Case
Partial<T> Partial<Type> Makes all properties optional - adds ? to each property Updates, patches, partial configs
Required<T> Required<Type> Makes all properties required - removes ? from properties Validation, complete objects
Readonly<T> Readonly<Type> Makes all properties readonly - adds readonly modifier Immutable data, constants

Example: Partial<T> for optional properties

interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

// All properties optional
type PartialUser = Partial<User>;
// Result: { id?: number; name?: string; email?: string; age?: number; }

// Update function accepts partial data
function updateUser(id: number, updates: Partial<User>): User {
    const user = getUserById(id);
    return { ...user, ...updates };
}

updateUser(1, { name: "Alice" });           // OK
updateUser(2, { email: "alice@email.com" }); // OK
updateUser(3, {});                           // OK - no updates

// Partial for default configs
interface Config {
    host: string;
    port: number;
    ssl: boolean;
    timeout: number;
}

function createConfig(overrides: Partial<Config> = {}): Config {
    const defaults: Config = {
        host: "localhost",
        port: 3000,
        ssl: false,
        timeout: 5000
    };
    return { ...defaults, ...overrides };
}

Example: Required<T> to enforce completeness

interface Settings {
    theme?: "light" | "dark";
    fontSize?: number;
    notifications?: boolean;
}

// Make all required for validation
type CompleteSettings = Required<Settings>;
// Result: { theme: "light" | "dark"; fontSize: number; notifications: boolean; }

function validateSettings(settings: CompleteSettings): boolean {
    // All properties guaranteed to exist
    return settings.theme !== undefined &&
           settings.fontSize > 0 &&
           settings.notifications !== undefined;
}

// Readonly for immutable state
const config: Readonly<Config> = {
    host: "localhost",
    port: 3000,
    ssl: false,
    timeout: 5000
};

// config.port = 8080; // Error: Cannot assign to readonly property

4.2 Pick<T, K> and Omit<T, K> for Property Selection

Utility Type Syntax Description Use Case
Pick<T, K> Pick<Type, Keys> Select specific properties from type - creates subset DTOs, API responses, projections
Omit<T, K> Omit<Type, Keys> Remove specific properties from type - creates type without keys Exclude sensitive data, inheritance

Example: Pick<T, K> to select properties

interface User {
    id: number;
    name: string;
    email: string;
    password: string;
    role: string;
    createdAt: Date;
    updatedAt: Date;
}

// Pick only public-facing fields
type PublicUser = Pick<User, "id" | "name" | "email">;
// Result: { id: number; name: string; email: string; }

// Use for API responses
function getPublicProfile(userId: number): PublicUser {
    const user = findUser(userId);
    return {
        id: user.id,
        name: user.name,
        email: user.email
    };
}

// Pick for form fields
type LoginForm = Pick<User, "email" | "password">;
// Result: { email: string; password: string; }

// Pick with union
type UserIdentifiers = Pick<User, "id" | "email">;

// Pick for timestamps
type Timestamps = Pick<User, "createdAt" | "updatedAt">;

Example: Omit<T, K> to exclude properties

// Omit sensitive fields
type SafeUser = Omit<User, "password">;
// Result: All User fields except password

// Omit multiple fields
type UserWithoutMeta = Omit<User, "createdAt" | "updatedAt">;

// Create input type by omitting generated fields
type CreateUserInput = Omit<User, "id" | "createdAt" | "updatedAt">;
// Result: { name: string; email: string; password: string; role: string; }

function createUser(input: CreateUserInput): User {
    return {
        ...input,
        id: generateId(),
        createdAt: new Date(),
        updatedAt: new Date()
    };
}

// Omit for derived types
interface Product {
    id: string;
    name: string;
    price: number;
    discount: number;
}

type ProductInput = Omit<Product, "id">;
type ProductWithoutDiscount = Omit<Product, "discount">;

4.3 Record<K, T> and keyof Type Operators

Feature Syntax Description Use Case
Record<K, T> Record<Keys, Type> Create object type with specified keys and value type Dictionaries, maps, lookup tables
keyof keyof Type Union of all property keys as string/number literals Type-safe property access, mapping
typeof typeof value Extract type from value - works on values, not types Derive types from objects/functions

Example: Record<K, T> for dictionaries

// Basic Record
type UserRoles = "admin" | "user" | "guest";
type Permissions = Record<UserRoles, string[]>;

const permissions: Permissions = {
    admin: ["read", "write", "delete"],
    user: ["read", "write"],
    guest: ["read"]
};

// Record with string keys
type StringMap = Record<string, number>;
const scores: StringMap = {
    "Alice": 95,
    "Bob": 87,
    "Charlie": 92
};

// Record with template literal keys
type HTTPMethods = "GET" | "POST" | "PUT" | "DELETE";
type RouteHandlers = Record<HTTPMethods, (req: Request) => Response>;

const handlers: RouteHandlers = {
    GET: (req) => ({ status: 200, body: "GET" }),
    POST: (req) => ({ status: 201, body: "POST" }),
    PUT: (req) => ({ status: 200, body: "PUT" }),
    DELETE: (req) => ({ status: 204, body: "" })
};

// Record for configuration
type Environment = "development" | "staging" | "production";
type EnvConfig = Record<Environment, { apiUrl: string; debug: boolean }>;

const config: EnvConfig = {
    development: { apiUrl: "http://localhost:3000", debug: true },
    staging: { apiUrl: "https://staging.api.com", debug: true },
    production: { apiUrl: "https://api.com", debug: false }
};

Example: keyof for type-safe property access

interface Person {
    name: string;
    age: number;
    email: string;
}

// keyof extracts property keys
type PersonKeys = keyof Person;
// Result: "name" | "age" | "email"

// Type-safe property getter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person: Person = { name: "Alice", age: 30, email: "alice@email.com" };

let name = getProperty(person, "name");     // string
let age = getProperty(person, "age");       // number
// let x = getProperty(person, "invalid");  // Error!

// Type-safe property setter
function setProperty<T, K extends keyof T>(
    obj: T,
    key: K,
    value: T[K]
): void {
    obj[key] = value;
}

setProperty(person, "name", "Bob");  // OK
setProperty(person, "age", 25);      // OK
// setProperty(person, "age", "25"); // Error: wrong type

// keyof with mapped types
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type PersonGetters = Getters<Person>;
// Result: { getName: () => string; getAge: () => number; getEmail: () => string; }

4.4 Exclude<T, U> and Extract<T, U> for Union Manipulation

Utility Type Syntax Description Result
Exclude<T, U> Exclude<Union, Excluded> Remove types from union - filters out specified types Union without excluded types
Extract<T, U> Extract<Union, Extracted> Keep only specified types from union - filters to match Union containing only extracted types
NonNullable<T> NonNullable<Type> Remove null and undefined from type Type without null/undefined

Example: Exclude<T, U> to remove types

// Basic Exclude
type AllTypes = string | number | boolean | null | undefined;
type PrimitiveTypes = Exclude<AllTypes, null | undefined>;
// Result: string | number | boolean

// Exclude specific values
type Status = "pending" | "active" | "completed" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled">;
// Result: "pending" | "active" | "completed"

// Exclude function types
type MixedTypes = string | number | (() => void) | { id: number };
type NonFunctionTypes = Exclude<MixedTypes, Function>;
// Result: string | number | { id: number }

// Exclude keys from type
type UserKeys = keyof User;
type NonIdKeys = Exclude<UserKeys, "id">;

// Practical: exclude built-in methods
type OwnProperties<T> = Exclude<keyof T, keyof any[]>;

// Remove optional properties
type RequiredKeys<T> = Exclude<{
    [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T], undefined>;

Example: Extract<T, U> to filter types

// Basic Extract
type AllTypes = string | number | boolean | null;
type NullableTypes = Extract<AllTypes, null | undefined>;
// Result: null

type StringOrNumber = Extract<AllTypes, string | number>;
// Result: string | number

// Extract specific literal types
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
type ReadMethods = Extract<HTTPMethod, "GET">;
// Result: "GET"

type WriteMethods = Extract<HTTPMethod, "POST" | "PUT" | "DELETE" | "PATCH">;
// Result: "POST" | "PUT" | "DELETE" | "PATCH"

// Extract function types
type Mixed = string | number | ((x: number) => number) | ((y: string) => string);
type Functions = Extract<Mixed, Function>;
// Result: ((x: number) => number) | ((y: string) => string)

// Extract by structure
type Shapes = 
    | { kind: "circle"; radius: number }
    | { kind: "square"; size: number }
    | { kind: "rectangle"; width: number; height: number };

type CircleShape = Extract<Shapes, { kind: "circle" }>;
// Result: { kind: "circle"; radius: number }

// NonNullable - remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// Result: string

4.5 ReturnType<T> and Parameters<T> for Function Types

Utility Type Syntax Description Extracts
ReturnType<T> ReturnType<Function> Extract return type from function type Function return type
Parameters<T> Parameters<Function> Extract parameter types as tuple Parameter tuple type
ConstructorParameters<T> ConstructorParameters<Class> Extract constructor parameter types Constructor params tuple
InstanceType<T> InstanceType<Constructor> Extract instance type from constructor Instance type
ThisParameterType<T> ThisParameterType<Fn> Extract this parameter type from function this type or unknown

Example: ReturnType<T> for type inference

// Basic ReturnType
function getUser() {
    return { id: 1, name: "Alice", email: "alice@email.com" };
}

type User = ReturnType<typeof getUser>;
// Result: { id: number; name: string; email: string; }

// ReturnType with generic function
function fetchData<T>(url: string): Promise<T> {
    return fetch(url).then(res => res.json());
}

type FetchReturn = ReturnType<typeof fetchData>;
// Result: Promise<unknown> (generic resolved to unknown)

// ReturnType from function type
type Handler = (event: Event) => void;
type HandlerReturn = ReturnType<Handler>;
// Result: void

// Complex function return
function getConfig() {
    return {
        api: { url: "https://api.com", timeout: 5000 },
        features: { darkMode: true, notifications: false }
    } as const;
}

type Config = ReturnType<typeof getConfig>;
// Infers readonly structure

// Array method return types
type MapReturn = ReturnType<typeof Array.prototype.map>;
type FilterReturn = ReturnType<typeof Array.prototype.filter>;

Example: Parameters<T> for parameter types

// Basic Parameters
function createUser(name: string, age: number, email: string) {
    return { name, age, email };
}

type CreateUserParams = Parameters<typeof createUser>;
// Result: [name: string, age: number, email: string]

// Use extracted parameters
function logCreateUser(...args: CreateUserParams) {
    console.log("Creating user:", args);
    return createUser(...args);
}

// Parameters with function type
type HandlerFn = (event: MouseEvent, options: { passive: boolean }) => void;
type HandlerParams = Parameters<HandlerFn>;
// Result: [event: MouseEvent, options: { passive: boolean }]

// Extract specific parameter
type FirstParam<T extends (...args: any[]) => any> = Parameters<T>[0];

type Handler = (id: number, data: string) => void;
type FirstArg = FirstParam<Handler>;  // number

// Combine with ReturnType
function transform(input: string): number {
    return parseInt(input);
}

type TransformParams = Parameters<typeof transform>;  // [string]
type TransformReturn = ReturnType<typeof transform>;  // number

4.6 NonNullable<T> and InstanceType<T> Helpers

Utility Type Syntax Description Use Case
NonNullable<T> NonNullable<Type> Remove null and undefined from type Validation, type narrowing
InstanceType<T> InstanceType<Constructor> Get instance type from constructor function Factory patterns, DI containers
Awaited<T> TS 4.5+ Awaited<Promise> Recursively unwrap Promise types Async function return types

Example: NonNullable<T> for type safety

// Basic NonNullable
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// Result: string

// NonNullable with union
type Value = string | number | null | undefined | boolean;
type NonNullValue = NonNullable<Value>;
// Result: string | number | boolean

// Filter array to non-null values
function filterNulls<T>(arr: (T | null | undefined)[]): NonNullable<T>[] {
    return arr.filter((item): item is NonNullable<T> => item != null);
}

const mixed = [1, null, 2, undefined, 3];
const numbers = filterNulls(mixed);  // number[]

// Safe access with NonNullable
type User = { name: string; email?: string | null };

function getEmail(user: User): NonNullable<User["email"]> | "no-email" {
    return user.email ?? "no-email";
}

// NonNullable in generic constraints
function process<T>(value: NonNullable<T>): void {
    // value is guaranteed not null/undefined
    console.log(value);
}

Example: InstanceType<T> for constructor types

// Basic InstanceType
class User {
    constructor(public name: string, public age: number) {}
}

type UserInstance = InstanceType<typeof User>;
// Result: User

// Factory function with InstanceType
function createInstance<T extends new (...args: any[]) => any>(
    constructor: T,
    ...args: ConstructorParameters<T>
): InstanceType<T> {
    return new constructor(...args);
}

const user = createInstance(User, "Alice", 30);  // User

// InstanceType with abstract classes
abstract class Animal {
    abstract makeSound(): void;
}

class Dog extends Animal {
    makeSound() { console.log("Woof!"); }
}

type DogInstance = InstanceType<typeof Dog>;  // Dog

// Registry pattern
class Registry {
    private constructors = new Map<string, new (...args: any[]) => any>();
    
    register<T extends new (...args: any[]) => any>(
        name: string,
        constructor: T
    ): void {
        this.constructors.set(name, constructor);
    }
    
    create<T extends new (...args: any[]) => any>(
        name: string,
        ...args: any[]
    ): InstanceType<T> | undefined {
        const Constructor = this.constructors.get(name) as T;
        return Constructor ? new Constructor(...args) : undefined;
    }
}

Example: Awaited<T> for Promise unwrapping

// Awaited unwraps Promise types
type StringPromise = Promise<string>;
type UnwrappedString = Awaited<StringPromise>;
// Result: string

// Deep unwrapping
type NestedPromise = Promise<Promise<number>>;
type UnwrappedNumber = Awaited<NestedPromise>;
// Result: number

// Awaited with async function
async function fetchUser(): Promise<{ id: number; name: string }> {
    return { id: 1, name: "Alice" };
}

type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// Result: { id: number; name: string }

// Non-Promise types pass through
type NotPromise = Awaited<string>;
// Result: string

// Awaited with union
type MixedPromises = Promise<string> | Promise<number> | boolean;
type UnwrappedMixed = Awaited<MixedPromises>;
// Result: string | number | boolean

Utility Types Reference

Property Modifiers

  • Partial<T> - Optional
  • Required<T> - Required
  • Readonly<T> - Immutable

Property Selection

  • Pick<T, K> - Include
  • Omit<T, K> - Exclude
  • Record<K, T> - Create

Union Manipulation

  • Exclude<T, U> - Remove
  • Extract<T, U> - Filter
  • NonNullable<T> - Clean

Function Types

  • ReturnType<T> - Return type
  • Parameters<T> - Param tuple
  • ConstructorParameters<T> - Constructor args

Class Types

  • InstanceType<T> - Instance
  • ThisParameterType<T> - this type
  • Awaited<T> - Unwrap Promise

5. Interface Design and Type Contracts

5.1 Interface Declaration and Extension

Feature Syntax Description Use Case
Basic Interface interface Name { } Define object shape with properties and methods Object contracts, API shapes
Interface Extension interface B extends A Inherit properties from parent interface Type hierarchies, specialization
Multiple Extension extends A, B, C Extend multiple interfaces simultaneously Mixin patterns, composition
Hybrid Type Callable + properties Object that is both callable and has properties Function objects, jQuery-like APIs

Example: Basic interface declaration

// Basic interface
interface User {
    id: number;
    name: string;
    email: string;
}

const user: User = {
    id: 1,
    name: "Alice",
    email: "alice@email.com"
};

// Interface with methods
interface Calculator {
    add(a: number, b: number): number;
    subtract(a: number, b: number): number;
}

const calc: Calculator = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b
};

// Nested interfaces
interface Address {
    street: string;
    city: string;
    country: string;
}

interface Person {
    name: string;
    address: Address;
}

Example: Interface extension

// Single extension
interface Animal {
    name: string;
    age: number;
}

interface Dog extends Animal {
    breed: string;
    bark(): void;
}

const dog: Dog = {
    name: "Buddy",
    age: 3,
    breed: "Golden Retriever",
    bark() { console.log("Woof!"); }
};

// Multiple extension
interface Timestamped {
    createdAt: Date;
    updatedAt: Date;
}

interface Versioned {
    version: number;
}

interface Document extends Timestamped, Versioned {
    id: string;
    content: string;
}

// Override inherited properties
interface Shape {
    color: string;
}

interface ColoredSquare extends Shape {
    color: "red" | "blue" | "green";  // Narrowed type
    size: number;
}

// Extension chain
interface Entity {
    id: string;
}

interface NamedEntity extends Entity {
    name: string;
}

interface User extends NamedEntity {
    email: string;
}

5.2 Optional Properties and Readonly Modifiers

Modifier Syntax Description Use Case
Optional Property prop?: Type Property may be undefined or missing Partial data, optional fields
Readonly Property readonly prop: Type Property cannot be reassigned after initialization Immutable data, constants
Readonly Optional readonly prop?: Type Combine both modifiers - optional and immutable Optional config values

Example: Optional properties

// Optional properties
interface UserProfile {
    id: number;
    name: string;
    email: string;
    phone?: string;          // Optional
    avatar?: string;         // Optional
    bio?: string;            // Optional
}

// Valid with or without optional properties
const user1: UserProfile = {
    id: 1,
    name: "Alice",
    email: "alice@email.com"
};

const user2: UserProfile = {
    id: 2,
    name: "Bob",
    email: "bob@email.com",
    phone: "555-1234",
    avatar: "avatar.jpg"
};

// Function accepting optional properties
function createProfile(data: UserProfile): void {
    console.log(data.name);
    // Check optional properties before use
    if (data.phone) {
        console.log(`Phone: ${data.phone}`);
    }
}

Example: Readonly properties

// Readonly properties
interface Config {
    readonly apiUrl: string;
    readonly apiKey: string;
    readonly timeout: number;
    retries: number;  // Mutable
}

const config: Config = {
    apiUrl: "https://api.example.com",
    apiKey: "secret-key",
    timeout: 5000,
    retries: 3
};

// config.apiUrl = "https://new-url.com";  // Error: readonly
// config.timeout = 10000;                  // Error: readonly
config.retries = 5;  // OK - mutable

// Readonly arrays and objects
interface Data {
    readonly tags: readonly string[];
    readonly metadata: Readonly<{ key: string; value: string }>;
}

const data: Data = {
    tags: ["typescript", "tutorial"],
    metadata: { key: "author", value: "Alice" }
};

// data.tags.push("new");              // Error: readonly array
// data.metadata.key = "new";          // Error: readonly object

// Readonly with optional
interface Settings {
    readonly id: string;
    readonly theme?: "light" | "dark";
    readonly fontSize?: number;
}

5.3 Function Types and Method Signatures in Interfaces

Pattern Syntax Description Example
Method Signature method(args): Type Method within interface - regular syntax getName(): string
Function Property prop: (args) => Type Property that is a function - arrow syntax callback: () => void
Call Signature (args): Type Make interface itself callable (x: number): string
Construct Signature new (args): Type Constructor function signature new (name: string): User
Overloaded Methods Multiple signatures Method with multiple call signatures Different parameter combinations

Example: Method signatures

// Method signatures
interface Repository<T> {
    find(id: string): T | undefined;
    findAll(): T[];
    save(item: T): void;
    delete(id: string): boolean;
    count(): number;
}

// Function properties vs method signatures
interface EventEmitter {
    // Method signature (preferred)
    on(event: string, handler: Function): void;
    off(event: string, handler: Function): void;
    
    // Function property (allows reassignment)
    emit: (event: string, ...args: any[]) => void;
}

// Optional methods
interface Logger {
    log(message: string): void;
    error(message: string): void;
    warn?(message: string): void;  // Optional method
    debug?(message: string): void; // Optional method
}

// Generic method signatures
interface Transformer {
    transform<T, U>(input: T): U;
    validate<T>(data: T): boolean;
}

Example: Call and construct signatures

// Call signature - callable interface
interface StringFormatter {
    (input: string): string;  // Call signature
    prefix: string;            // Also has properties
    suffix: string;
}

function createFormatter(pre: string, suf: string): StringFormatter {
    const formatter = ((input: string) => {
        return `${formatter.prefix}${input}${formatter.suffix}`;
    }) as StringFormatter;
    
    formatter.prefix = pre;
    formatter.suffix = suf;
    return formatter;
}

const format = createFormatter("[", "]");
console.log(format("test"));  // "[test]"

// Construct signature - constructor interface
interface UserConstructor {
    new (name: string, age: number): User;
}

class User {
    constructor(public name: string, public age: number) {}
}

function createUser(ctor: UserConstructor, name: string, age: number): User {
    return new ctor(name, age);
}

const user = createUser(User, "Alice", 30);

// Hybrid type - callable with properties
interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

Example: Method overloading in interfaces

// Overloaded method signatures
interface Formatter {
    format(value: string): string;
    format(value: number): string;
    format(value: boolean): string;
    format(value: Date): string;
}

const formatter: Formatter = {
    format(value: string | number | boolean | Date): string {
        if (typeof value === "string") return value;
        if (typeof value === "number") return value.toString();
        if (typeof value === "boolean") return value ? "true" : "false";
        return value.toISOString();
    }
};

// Generic overloads
interface Parser {
    parse(input: string): any;
    parse<T>(input: string, type: new () => T): T;
}

// Rest parameters in interface
interface Logger {
    log(message: string, ...args: any[]): void;
}

5.4 Index Signatures for Dynamic Properties

Type Syntax Description Use Case
String Index [key: string]: Type Any string key maps to specified type Dictionaries, dynamic objects
Number Index [index: number]: Type Numeric indices for array-like structures Array-like objects, tuples
Symbol Index TS 4.4+ [key: symbol]: Type Symbol keys for unique properties Well-known symbols, metadata
Template Literal Index [K: `prefix${string}`] Pattern-based property keys Prefixed properties, conventions

Example: String index signatures

// Basic string index signature
interface Dictionary<T> {
    [key: string]: T;
}

const scores: Dictionary<number> = {
    "Alice": 95,
    "Bob": 87,
    "Charlie": 92
};

// Mixed: known properties + index signature
interface UserData {
    id: number;
    name: string;
    [key: string]: any;  // Additional dynamic properties
}

const userData: UserData = {
    id: 1,
    name: "Alice",
    age: 30,              // OK - additional property
    email: "alice@email.com"  // OK
};

// Readonly index signature
interface ReadonlyDict {
    readonly [key: string]: string;
}

const dict: ReadonlyDict = { a: "hello" };
// dict.a = "world";  // Error: readonly
// dict.b = "new";    // Error: readonly

// Constrained index signature
interface NumberDict {
    [key: string]: number;
    length: number;  // OK - number type
    // name: string; // Error: must be number
}

Example: Number and advanced index signatures

// Number index signature
interface StringArray {
    [index: number]: string;
}

const arr: StringArray = ["a", "b", "c"];
const first: string = arr[0];

// Both string and number index
interface MixedArray {
    [index: number]: string;
    [key: string]: string | number;
    length: number;
}

// Template literal index signature (TS 4.4+)
interface DataAttributes {
    [key: `data-${string}`]: string;
}

const attrs: DataAttributes = {
    "data-id": "123",
    "data-name": "test",
    // "id": "456"  // Error: must match pattern
};

// Generic index signature
interface Cache<T> {
    [key: string]: T | undefined;
    get(key: string): T | undefined;
    set(key: string, value: T): void;
}

// Symbol index signature (TS 4.4+)
interface Symbolized {
    [key: symbol]: any;
}

const sym = Symbol("id");
const obj: Symbolized = {
    [sym]: 123
};

5.5 Interface Merging and Declaration Merging

Concept Description Use Case
Declaration Merging Multiple interface declarations with same name merge into one Augmenting libraries, modules
Module Augmentation Add types to existing modules or libraries Extending third-party types
Global Augmentation Add properties to global namespace Window, process extensions
Namespace Merging Interfaces can merge with namespaces/functions Hybrid declarations

Example: Declaration merging

// Basic interface merging
interface User {
    id: number;
    name: string;
}

interface User {
    email: string;
    age: number;
}

// Merged result:
// interface User {
//     id: number;
//     name: string;
//     email: string;
//     age: number;
// }

const user: User = {
    id: 1,
    name: "Alice",
    email: "alice@email.com",
    age: 30
};

// Method overloading via merging
interface Calculator {
    add(a: number, b: number): number;
}

interface Calculator {
    add(a: string, b: string): string;
}

// Both overloads available
const calc: Calculator = {
    add(a: any, b: any): any {
        return a + b;
    }
};

Example: Module and global augmentation

// Augment existing module
declare module "express" {
    interface Request {
        user?: {
            id: string;
            name: string;
        };
    }
}

// Global augmentation
declare global {
    interface Window {
        customProperty: string;
        myAPI: {
            version: string;
            init(): void;
        };
    }
    
    interface Array<T> {
        customMethod(): T[];
    }
}

// Now available globally
window.customProperty = "value";
window.myAPI.init();

// Augment NodeJS global
declare global {
    namespace NodeJS {
        interface ProcessEnv {
            CUSTOM_VAR: string;
            API_KEY: string;
        }
    }
}

// Type-safe environment variables
const apiKey = process.env.API_KEY;  // string
Note: Interface merging only works with interfaces, not type aliases. This is one key difference between interfaces and types. Use merging for library augmentation and extending third-party types.

5.6 Implementing Interfaces in Classes

Feature Syntax Description Use Case
Single Implementation class X implements I Class must satisfy interface contract Type contracts, polymorphism
Multiple Implementation implements I1, I2, I3 Class implements multiple interfaces Multiple capabilities, composition
Interface + Extension extends A implements I Combine inheritance and interface implementation OOP patterns, frameworks
Generic Implementation class X<T> implements I<T> Implement generic interface with type parameter Generic containers, adapters

Example: Basic interface implementation

// Define interface
interface Printable {
    print(): void;
}

interface Serializable {
    serialize(): string;
    deserialize(data: string): void;
}

// Implement single interface
class Document implements Printable {
    constructor(private content: string) {}
    
    print(): void {
        console.log(this.content);
    }
}

// Implement multiple interfaces
class DataModel implements Printable, Serializable {
    constructor(private data: any) {}
    
    print(): void {
        console.log(JSON.stringify(this.data, null, 2));
    }
    
    serialize(): string {
        return JSON.stringify(this.data);
    }
    
    deserialize(data: string): void {
        this.data = JSON.parse(data);
    }
}

// Interface ensures implementation
// class InvalidDoc implements Printable {
//     // Error: missing print() method
// }

Example: Generic interface implementation

// Generic interface
interface Repository<T> {
    find(id: string): T | undefined;
    findAll(): T[];
    save(item: T): void;
    delete(id: string): boolean;
}

// Implement with specific type
class UserRepository implements Repository<User> {
    private users: Map<string, User> = new Map();
    
    find(id: string): User | undefined {
        return this.users.get(id);
    }
    
    findAll(): User[] {
        return Array.from(this.users.values());
    }
    
    save(user: User): void {
        this.users.set(user.id.toString(), user);
    }
    
    delete(id: string): boolean {
        return this.users.delete(id);
    }
}

// Generic class implementing generic interface
class GenericRepository<T extends { id: string }> implements Repository<T> {
    private items = new Map<string, T>();
    
    find(id: string): T | undefined {
        return this.items.get(id);
    }
    
    findAll(): T[] {
        return Array.from(this.items.values());
    }
    
    save(item: T): void {
        this.items.set(item.id, item);
    }
    
    delete(id: string): boolean {
        return this.items.delete(id);
    }
}

Example: Combining inheritance and interfaces

// Base class
class Entity {
    constructor(public id: string, public createdAt: Date) {}
}

// Interfaces
interface Timestamped {
    updatedAt: Date;
    touch(): void;
}

interface Versioned {
    version: number;
    incrementVersion(): void;
}

// Extend class and implement interfaces
class Document extends Entity implements Timestamped, Versioned {
    updatedAt: Date;
    version: number;
    
    constructor(
        id: string,
        public content: string
    ) {
        super(id, new Date());
        this.updatedAt = new Date();
        this.version = 1;
    }
    
    touch(): void {
        this.updatedAt = new Date();
    }
    
    incrementVersion(): void {
        this.version++;
        this.touch();
    }
}

// Polymorphism with interfaces
function printTimestamp(obj: Timestamped): void {
    console.log(`Updated: ${obj.updatedAt}`);
}

const doc = new Document("1", "Hello");
printTimestamp(doc);  // OK - Document implements Timestamped

Interface Design Best Practices

  • Use interfaces for object shapes and public contracts
  • Prefer readonly for immutable properties
  • Use optional properties for partial data
  • Apply index signatures for flexible dictionaries
  • Leverage declaration merging for library augmentation
  • Implement interfaces in classes for polymorphism
  • Use method signatures over function properties for better type checking

6. Classes and Object-Oriented Programming

6.1 Class Declaration and Constructor Functions

Feature Syntax Description Use Case
Class Declaration class Name { } Define blueprint for objects with properties and methods OOP patterns, encapsulation
Constructor constructor(args) { } Initialize instance when object created with new Object initialization, DI
Class Expression const C = class { } Anonymous or named class assigned to variable Factory patterns, conditional classes
Generic Class class Name<T> { } Class with type parameters for reusability Collections, containers

Example: Basic class declaration

// Basic class
class User {
    id: number;
    name: string;
    
    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
    
    greet(): string {
        return `Hello, I'm ${this.name}`;
    }
}

const user = new User(1, "Alice");
console.log(user.greet());  // "Hello, I'm Alice"

// Class with optional constructor parameters
class Product {
    constructor(
        public name: string,
        public price: number,
        public discount?: number
    ) {}
    
    getFinalPrice(): number {
        return this.discount 
            ? this.price * (1 - this.discount)
            : this.price;
    }
}

// Class expression
const Animal = class {
    constructor(public name: string) {}
    
    makeSound(): void {
        console.log("Some sound");
    }
};

Example: Generic classes

// Generic class
class Box<T> {
    private value: T;
    
    constructor(value: T) {
        this.value = value;
    }
    
    getValue(): T {
        return this.value;
    }
    
    setValue(value: T): void {
        this.value = value;
    }
}

const numberBox = new Box<number>(42);
const stringBox = new Box("hello");  // Type inferred

// Generic class with constraints
class Collection<T extends { id: string }> {
    private items: T[] = [];
    
    add(item: T): void {
        this.items.push(item);
    }
    
    findById(id: string): T | undefined {
        return this.items.find(item => item.id === id);
    }
}

6.2 Property Declaration and Initialization

Pattern Syntax Description Initialization
Declared Property prop: Type; Property with type annotation - must be initialized Constructor or inline
Initialized Property prop: Type = value; Property with default value Inline initialization
Optional Property prop?: Type; May be undefined - no initialization required Optional
Definite Assignment prop!: Type; Assert property will be initialized - skip strict check External/async

Example: Property initialization patterns

// Various initialization patterns
class User {
    // Inline initialization
    id: number = 0;
    
    // Declared, initialized in constructor
    name: string;
    
    // Default value
    role: string = "user";
    
    // Optional property
    email?: string;
    
    // Definite assignment assertion
    token!: string;  // Will be set by external method
    
    constructor(name: string) {
        this.name = name;
    }
    
    setToken(token: string): void {
        this.token = token;
    }
}

// Complex initialization
class Config {
    // Readonly after initialization
    readonly apiUrl: string;
    
    // Computed property
    timestamp: Date = new Date();
    
    // Array initialization
    tags: string[] = [];
    
    // Object initialization
    metadata: Record<string, any> = {};
    
    constructor(apiUrl: string) {
        this.apiUrl = apiUrl;
    }
}

6.3 Access Modifiers

Modifier Visible To Description Use Case
public Everywhere Default - accessible from anywhere Public API, external access
private Same class only Only accessible within declaring class Implementation details, encapsulation
protected Class + subclasses Accessible in class and derived classes Inheritance, template methods
readonly Immutable after init Cannot be reassigned after initialization Constants, configuration
#private ES2022 Runtime private True runtime privacy via JavaScript private fields True encapsulation

Example: Access modifiers

class BankAccount {
    // Public - accessible everywhere
    public accountNumber: string;
    
    // Private - only within class
    private balance: number;
    
    // Protected - class and subclasses
    protected owner: string;
    
    // Readonly - cannot change after initialization
    readonly createdAt: Date;
    
    // ES2022 private field (runtime private)
    #pin: string;
    
    constructor(accountNumber: string, owner: string, pin: string) {
        this.accountNumber = accountNumber;
        this.balance = 0;
        this.owner = owner;
        this.createdAt = new Date();
        this.#pin = pin;
    }
    
    // Public method
    public deposit(amount: number): void {
        this.balance += amount;  // OK - private accessible here
    }
    
    // Private method
    private validatePin(pin: string): boolean {
        return this.#pin === pin;
    }
    
    // Public method using private method
    public withdraw(amount: number, pin: string): boolean {
        if (!this.validatePin(pin)) {
            return false;
        }
        if (this.balance >= amount) {
            this.balance -= amount;
            return true;
        }
        return false;
    }
    
    // Public getter for private property
    public getBalance(): number {
        return this.balance;
    }
}

const account = new BankAccount("123456", "Alice", "1234");
account.deposit(100);           // OK - public
console.log(account.accountNumber);  // OK - public
// console.log(account.balance);     // Error - private
// console.log(account.#pin);        // Error - private field

Example: Protected members in inheritance

class Employee {
    protected employeeId: string;
    private salary: number;
    
    constructor(id: string, salary: number) {
        this.employeeId = id;
        this.salary = salary;
    }
    
    protected getSalary(): number {
        return this.salary;
    }
}

class Manager extends Employee {
    constructor(id: string, salary: number, private department: string) {
        super(id, salary);
    }
    
    // Can access protected members
    getDetails(): string {
        return `ID: ${this.employeeId}, Dept: ${this.department}`;
    }
    
    // Can call protected methods
    getSalaryWithBonus(): number {
        return this.getSalary() * 1.2;
    }
    
    // Cannot access private members
    // getBadSalary(): number {
    //     return this.salary;  // Error - private to Employee
    // }
}

const manager = new Manager("M001", 80000, "IT");
console.log(manager.getDetails());  // OK
// console.log(manager.employeeId);  // Error - protected

6.4 Static Members and Static Initialization Blocks

Feature Syntax Description Use Case
Static Property static prop: Type Class-level property - shared across all instances Constants, counters, config
Static Method static method() { } Called on class, not instance - no access to this Utilities, factories, helpers
Static Block TS 4.4+ static { } Initialize static properties with complex logic Static initialization, setup
Static Access Modifiers private static Combine static with access modifiers Internal static helpers

Example: Static members

class User {
    // Static property - shared across instances
    static totalUsers: number = 0;
    
    // Static readonly
    static readonly MAX_USERS: number = 1000;
    
    // Private static
    private static nextId: number = 1;
    
    id: number;
    name: string;
    
    constructor(name: string) {
        this.id = User.nextId++;
        this.name = name;
        User.totalUsers++;
    }
    
    // Static method
    static getUserCount(): number {
        return User.totalUsers;
    }
    
    // Static factory method
    static createAdmin(name: string): User {
        const user = new User(name);
        // Additional admin setup
        return user;
    }
    
    // Static utility
    static isValidEmail(email: string): boolean {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
    }
}

// Access static members on class
console.log(User.totalUsers);        // 0
console.log(User.MAX_USERS);         // 1000
console.log(User.getUserCount());    // 0

const user1 = new User("Alice");
const user2 = new User("Bob");

console.log(User.totalUsers);        // 2
console.log(User.isValidEmail("test@email.com"));  // true

Example: Static initialization blocks

class Config {
    static apiUrl: string;
    static timeout: number;
    static features: Map<string, boolean>;
    
    // Static initialization block
    static {
        // Complex initialization logic
        const env = process.env.NODE_ENV || "development";
        
        if (env === "production") {
            Config.apiUrl = "https://api.production.com";
            Config.timeout = 5000;
        } else {
            Config.apiUrl = "https://api.dev.com";
            Config.timeout = 30000;
        }
        
        // Initialize complex structures
        Config.features = new Map();
        Config.features.set("darkMode", true);
        Config.features.set("notifications", false);
        
        console.log("Config initialized for", env);
    }
    
    static getFeature(name: string): boolean {
        return Config.features.get(name) ?? false;
    }
}

// Static block runs when class first evaluated
console.log(Config.apiUrl);  // URL based on environment

6.5 Abstract Classes and Abstract Methods

Feature Syntax Description Use Case
Abstract Class abstract class Name Cannot be instantiated - must be extended Base classes, templates
Abstract Method abstract method(): Type No implementation - must be implemented by subclass Contract enforcement, polymorphism
Abstract Property abstract prop: Type Property declaration without implementation Required properties in subclasses
Concrete Methods Regular methods in abstract class Provide default implementation - can be overridden Shared behavior, template methods

Example: Abstract classes

// Abstract base class
abstract class Shape {
    // Abstract property
    abstract name: string;
    
    // Concrete property
    color: string = "black";
    
    // Abstract methods - must be implemented
    abstract getArea(): number;
    abstract getPerimeter(): number;
    
    // Concrete method - shared implementation
    describe(): string {
        return `${this.name} with area ${this.getArea()}`;
    }
    
    // Concrete method using abstract methods (template method pattern)
    display(): void {
        console.log(`${this.name}:`);
        console.log(`  Area: ${this.getArea()}`);
        console.log(`  Perimeter: ${this.getPerimeter()}`);
    }
}

// Cannot instantiate abstract class
// const shape = new Shape();  // Error!

// Concrete implementation
class Circle extends Shape {
    name = "Circle";
    
    constructor(public radius: number) {
        super();
    }
    
    getArea(): number {
        return Math.PI * this.radius ** 2;
    }
    
    getPerimeter(): number {
        return 2 * Math.PI * this.radius;
    }
}

class Rectangle extends Shape {
    name = "Rectangle";
    
    constructor(public width: number, public height: number) {
        super();
    }
    
    getArea(): number {
        return this.width * this.height;
    }
    
    getPerimeter(): number {
        return 2 * (this.width + this.height);
    }
}

const circle = new Circle(5);
circle.display();  // Uses inherited display() method

const rect = new Rectangle(4, 6);
console.log(rect.describe());  // "Rectangle with area 24"

6.6 Class Inheritance and super Keyword

Feature Syntax Description Use Case
Inheritance class B extends A Derive class from parent - inherit properties/methods Code reuse, specialization
super() Call super(args) Call parent constructor - required in derived constructor Initialize parent class
super.method() super.methodName() Call parent method from overridden method Extend parent behavior
Method Override Same signature as parent Replace parent method implementation Polymorphism, specialization

Example: Class inheritance

// Base class
class Animal {
    constructor(public name: string, public age: number) {}
    
    makeSound(): void {
        console.log("Some generic sound");
    }
    
    describe(): string {
        return `${this.name} is ${this.age} years old`;
    }
}

// Derived class
class Dog extends Animal {
    constructor(name: string, age: number, public breed: string) {
        super(name, age);  // Must call parent constructor
    }
    
    // Override parent method
    makeSound(): void {
        console.log("Woof! Woof!");
    }
    
    // New method specific to Dog
    fetch(): void {
        console.log(`${this.name} is fetching`);
    }
    
    // Override and extend parent method
    describe(): string {
        const baseDescription = super.describe();  // Call parent
        return `${baseDescription} and is a ${this.breed}`;
    }
}

const dog = new Dog("Buddy", 3, "Golden Retriever");
dog.makeSound();      // "Woof! Woof!" (overridden)
dog.fetch();          // "Buddy is fetching" (new method)
console.log(dog.describe());  // Uses both parent and child logic

Example: Inheritance chain

// Multi-level inheritance
class Entity {
    constructor(public id: string) {}
}

class TimestampedEntity extends Entity {
    createdAt: Date;
    updatedAt: Date;
    
    constructor(id: string) {
        super(id);
        this.createdAt = new Date();
        this.updatedAt = new Date();
    }
    
    touch(): void {
        this.updatedAt = new Date();
    }
}

class User extends TimestampedEntity {
    constructor(id: string, public name: string, public email: string) {
        super(id);
    }
    
    updateProfile(name: string, email: string): void {
        this.name = name;
        this.email = email;
        this.touch();  // Inherited from TimestampedEntity
    }
}

const user = new User("u1", "Alice", "alice@email.com");
// Has properties from all levels: id, createdAt, updatedAt, name, email

6.7 Getters and Setters with Type Safety

Feature Syntax Description Use Case
Getter get prop(): Type Computed property - executed when accessed Derived values, validation
Setter set prop(value: Type) Intercept property assignment - validate/transform Validation, side effects
Readonly via Getter Getter without setter Property that can be read but not set externally Computed readonly properties
Access Modifiers private get/set Control visibility of accessors Encapsulation

Example: Getters and setters

class User {
    private _email: string = "";
    private _age: number = 0;
    
    // Getter
    get email(): string {
        return this._email;
    }
    
    // Setter with validation
    set email(value: string) {
        if (!value.includes("@")) {
            throw new Error("Invalid email format");
        }
        this._email = value;
    }
    
    // Getter with transformation
    get age(): number {
        return this._age;
    }
    
    // Setter with validation
    set age(value: number) {
        if (value < 0 || value > 150) {
            throw new Error("Invalid age");
        }
        this._age = value;
    }
    
    // Readonly computed property (getter only)
    get isAdult(): boolean {
        return this._age >= 18;
    }
}

const user = new User();
user.email = "alice@email.com";  // Calls setter
console.log(user.email);          // Calls getter
console.log(user.isAdult);        // Computed property
// user.isAdult = true;           // Error: no setter

Example: Advanced accessor patterns

class Rectangle {
    constructor(private _width: number, private _height: number) {}
    
    // Width accessor
    get width(): number {
        return this._width;
    }
    
    set width(value: number) {
        if (value <= 0) throw new Error("Width must be positive");
        this._width = value;
    }
    
    // Height accessor
    get height(): number {
        return this._height;
    }
    
    set height(value: number) {
        if (value <= 0) throw new Error("Height must be positive");
        this._height = value;
    }
    
    // Computed properties
    get area(): number {
        return this._width * this._height;
    }
    
    get perimeter(): number {
        return 2 * (this._width + this._height);
    }
    
    // Setter that affects multiple properties
    set dimensions(value: { width: number; height: number }) {
        this.width = value.width;
        this.height = value.height;
    }
}

const rect = new Rectangle(10, 5);
console.log(rect.area);       // 50
rect.width = 20;
console.log(rect.area);       // 100
rect.dimensions = { width: 15, height: 10 };
console.log(rect.perimeter);  // 50

6.8 Parameter Properties and Shorthand Syntax

Feature Syntax Description Benefit
Parameter Property constructor(public prop) Declare and initialize property in constructor parameter Reduced boilerplate
Public Parameter public name: Type Creates public property automatically Concise syntax
Private Parameter private name: Type Creates private property automatically Encapsulation shorthand
Protected Parameter protected name: Type Creates protected property automatically Inheritance shorthand
Readonly Parameter readonly name: Type Creates readonly property automatically Immutability shorthand

Example: Parameter properties (before/after)

// Traditional verbose syntax
class User {
    id: number;
    name: string;
    email: string;
    
    constructor(id: number, name: string, email: string) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
}

// Parameter properties - concise equivalent
class UserShort {
    constructor(
        public id: number,
        public name: string,
        public email: string
    ) {}
}

// Both create identical classes!
const user1 = new User(1, "Alice", "alice@email.com");
const user2 = new UserShort(1, "Alice", "alice@email.com");

// Mixed: parameter properties + regular properties
class Product {
    discount: number = 0;  // Regular property
    
    constructor(
        public id: string,
        public name: string,
        private _price: number
    ) {}
    
    get price(): number {
        return this._price * (1 - this.discount);
    }
}

Example: Parameter properties with modifiers

class BankAccount {
    constructor(
        readonly accountNumber: string,      // Readonly
        private balance: number,              // Private
        protected owner: string,              // Protected
        public bankName: string               // Public
    ) {}
    
    deposit(amount: number): void {
        this.balance += amount;  // Can modify private property
    }
    
    getBalance(): number {
        return this.balance;
    }
}

const account = new BankAccount("12345", 1000, "Alice", "MyBank");
console.log(account.accountNumber);  // OK - readonly
console.log(account.bankName);       // OK - public
// console.log(account.balance);     // Error - private
// console.log(account.owner);       // Error - protected
// account.accountNumber = "99999";  // Error - readonly

// Readonly parameter properties for immutable data
class Point {
    constructor(
        readonly x: number,
        readonly y: number
    ) {}
    
    distanceFromOrigin(): number {
        return Math.sqrt(this.x ** 2 + this.y ** 2);
    }
}

const point = new Point(3, 4);
console.log(point.x, point.y);        // 3, 4
// point.x = 5;                       // Error - readonly

OOP Best Practices

  • Use parameter properties to reduce boilerplate
  • Apply private to implementation details for encapsulation
  • Use protected for properties needed in inheritance
  • Leverage abstract classes for template methods
  • Prefer getters/setters for computed or validated properties
  • Use static for utilities and factory methods
  • Mark immutable properties as readonly
  • Always call super() first in derived class constructors

7. Function Types and Advanced Function Features

7.1 Function Type Expressions and Call Signatures

Pattern Syntax Description Use Case
Function Type Expression (args) => ReturnType Inline function type - arrow syntax Callbacks, function parameters
Call Signature { (args): ReturnType } Function type in object/interface - can have properties Callable objects with properties
Type Alias for Function type Fn = (args) => Type Reusable named function type Shared function signatures
Generic Function Type <T>(arg: T) => T Function type with type parameters Generic callbacks, transformers
Void Return () => void Function returns nothing useful Event handlers, side effects

Example: Function type expressions

// Function type expression
type MathOperation = (a: number, b: number) => number;

const add: MathOperation = (a, b) => a + b;
const multiply: MathOperation = (a, b) => a * b;

// Function as parameter
function calculate(
    x: number,
    y: number,
    operation: (a: number, b: number) => number
): number {
    return operation(x, y);
}

console.log(calculate(5, 3, add));       // 8
console.log(calculate(5, 3, multiply));  // 15

// Generic function type
type Mapper<T, U> = (item: T) => U;

const toString: Mapper<number, string> = (n) => n.toString();
const toLength: Mapper<string, number> = (s) => s.length;

// Complex function types
type Predicate<T> = (item: T) => boolean;
type Transformer<T, U> = (input: T) => U;
type Handler = (event: Event) => void;
type AsyncFetcher<T> = (url: string) => Promise<T>;

Example: Call signatures

// Call signature with properties
interface Callable {
    (x: number): string;  // Call signature
    description: string;   // Property
    version: number;       // Property
}

function createCallable(): Callable {
    const fn = ((x: number) => x.toString()) as Callable;
    fn.description = "Converts number to string";
    fn.version = 1;
    return fn;
}

const callable = createCallable();
console.log(callable(42));          // "42"
console.log(callable.description);  // "Converts number to string"

// Multiple call signatures (overloads)
interface Formatter {
    (value: string): string;
    (value: number): string;
    (value: boolean): string;
}

const format: Formatter = (value: any): string => {
    return String(value);
};

// Constructor signature
interface Constructor {
    new (name: string): Object;
}

// Hybrid type - both callable and constructible
interface DateConstructor {
    new (value: number): Date;
    new (value: string): Date;
    (): string;
    now(): number;
}

7.2 Function Overloading and Implementation Signatures

Component Description Purpose
Overload Signatures Multiple function signatures without implementation Declare all valid call patterns
Implementation Signature Single signature with function body - must be compatible with all overloads Actual function implementation
Return Type Variation Different return types based on parameters Type-safe conditional returns
Parameter Variation Different number/types of parameters Flexible function API

Example: Function overloading

// Basic overloading
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(value: number | string): string | number {
    if (typeof value === "number") {
        return `ID: ${value}`;
    } else {
        return value.length;
    }
}

let result1 = getValue(123);      // string
let result2 = getValue("Alice");  // number

// Overloading with different parameter counts
function createElement(tag: string): HTMLElement;
function createElement(tag: string, content: string): HTMLElement;
function createElement(tag: string, content: string, className: string): HTMLElement;
function createElement(tag: string, content?: string, className?: string): HTMLElement {
    const element = document.createElement(tag);
    if (content) element.textContent = content;
    if (className) element.className = className;
    return element;
}

// Overloading with generics
function parse(value: string): any;
function parse<T>(value: string, reviver: (key: string, value: any) => any): T;
function parse<T>(value: string, reviver?: (key: string, value: any) => any): T | any {
    return JSON.parse(value, reviver);
}

// Overloading for conditional return types
function process(input: string): string;
function process(input: number): number;
function process(input: boolean): boolean;
function process(input: string | number | boolean): string | number | boolean {
    return input;
}

let str = process("hello");    // string
let num = process(42);         // number
let bool = process(true);      // boolean

Example: Advanced overloading patterns

// Overloading with union narrowing
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
    if (month !== undefined && day !== undefined) {
        return new Date(yearOrTimestamp, month - 1, day);
    } else {
        return new Date(yearOrTimestamp);
    }
}

const date1 = makeDate(1609459200000);     // Date from timestamp
const date2 = makeDate(2024, 1, 1);        // Date from parts

// Overloading with type predicates
function filter<T>(array: T[], predicate: (item: T) => boolean): T[];
function filter<T, S extends T>(
    array: T[],
    predicate: (item: T) => item is S
): S[];
function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
    return array.filter(predicate);
}

// Method overloading in classes
class Calculator {
    add(a: number, b: number): number;
    add(a: string, b: string): string;
    add(a: any, b: any): any {
        return a + b;
    }
}

const calc = new Calculator();
calc.add(1, 2);        // 3 (number)
calc.add("a", "b");    // "ab" (string)

7.3 Optional Parameters and Default Parameter Values

Feature Syntax Description Behavior
Optional Parameter param?: Type Parameter may be undefined or omitted Type is Type | undefined
Default Parameter param: Type = value Parameter has default value if not provided Type inferred from default
Optional After Required Optional must come after required params TypeScript enforces parameter order Compile-time enforcement
Destructured Defaults { x = 1, y = 2 } Default values in object destructuring Individual property defaults

Example: Optional parameters

// Basic optional parameters
function greet(name: string, greeting?: string): string {
    if (greeting) {
        return `${greeting}, ${name}!`;
    }
    return `Hello, ${name}!`;
}

console.log(greet("Alice"));              // "Hello, Alice!"
console.log(greet("Bob", "Hi"));          // "Hi, Bob!"

// Multiple optional parameters
function createUser(
    name: string,
    age?: number,
    email?: string
): { name: string; age?: number; email?: string } {
    return { name, age, email };
}

createUser("Alice");                      // { name: "Alice" }
createUser("Bob", 30);                    // { name: "Bob", age: 30 }
createUser("Charlie", 25, "c@email.com"); // All fields

// Optional with type checking
function log(message: string, level?: "info" | "warn" | "error"): void {
    const prefix = level ? `[${level.toUpperCase()}]` : "[LOG]";
    console.log(`${prefix} ${message}`);
}

Example: Default parameters

// Basic default parameters
function power(base: number, exponent: number = 2): number {
    return Math.pow(base, exponent);
}

console.log(power(3));      // 9 (3^2)
console.log(power(3, 3));   // 27 (3^3)

// Type inferred from default
function createArray(length = 10) {  // length: number
    return new Array(length);
}

// Complex default values
function fetchData(
    url: string,
    options: RequestInit = { method: "GET", headers: {} }
): Promise<Response> {
    return fetch(url, options);
}

// Default with destructuring
function drawRect({
    x = 0,
    y = 0,
    width = 100,
    height = 100,
    color = "black"
}: {
    x?: number;
    y?: number;
    width?: number;
    height?: number;
    color?: string;
} = {}): void {
    console.log(`Drawing ${width}x${height} rect at (${x},${y})`);
}

drawRect();                          // All defaults
drawRect({ x: 10, y: 20 });         // Partial values
drawRect({ width: 200, color: "red" }); // Mix defaults and values

// Default with type alias
type Options = {
    timeout?: number;
    retries?: number;
};

function request(url: string, { timeout = 5000, retries = 3 }: Options = {}): void {
    console.log(`Request to ${url} (timeout: ${timeout}, retries: ${retries})`);
}

7.4 Rest Parameters with Spread Syntax

Feature Syntax Description Type
Rest Parameters ...args: Type[] Capture variable number of arguments as array Array of Type
Typed Rest ...items: T[] Rest parameter with specific type Type-safe varargs
Tuple Rest ...args: [T, U, V] Rest parameter as tuple type Fixed types for each position
Rest Position Must be last parameter Only one rest parameter, at the end Language constraint
Spread in Call fn(...array) Spread array as individual arguments Array to arguments

Example: Rest parameters

// Basic rest parameters
function sum(...numbers: number[]): number {
    return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3));           // 6
console.log(sum(1, 2, 3, 4, 5));     // 15

// Rest with required parameters
function multiply(multiplier: number, ...numbers: number[]): number[] {
    return numbers.map(n => n * multiplier);
}

console.log(multiply(2, 1, 2, 3));   // [2, 4, 6]

// Generic rest parameters
function merge<T>(...objects: T[]): T {
    return Object.assign({}, ...objects);
}

const result = merge({ a: 1 }, { b: 2 }, { c: 3 });
// { a: 1, b: 2, c: 3 }

// Rest with different types
function logAll(prefix: string, ...messages: (string | number)[]): void {
    messages.forEach(msg => console.log(`${prefix}: ${msg}`));
}

logAll("INFO", "Starting", 123, "Complete");

// Tuple rest parameters
function process(...args: [string, number, boolean]): void {
    const [name, age, active] = args;
    console.log(`${name} is ${age} years old, active: ${active}`);
}

process("Alice", 30, true);

Example: Spread syntax

// Spread array to function arguments
const numbers = [1, 2, 3, 4, 5];
console.log(Math.max(...numbers));  // 5

// Spread with typed function
function greet(first: string, second: string, third: string): void {
    console.log(`${first}, ${second}, ${third}`);
}

const names: [string, string, string] = ["Alice", "Bob", "Charlie"];
greet(...names);  // Type-safe spread

// Rest in function type
type Logger = (...messages: string[]) => void;

const log: Logger = (...messages) => {
    console.log(messages.join(" "));
};

// Combining rest and spread
function combineArrays<T>(...arrays: T[][]): T[] {
    return arrays.flat();
}

const combined = combineArrays([1, 2], [3, 4], [5, 6]);
// [1, 2, 3, 4, 5, 6]

// Rest parameters with constraints
function createPair<T extends string | number>(...items: [T, T]): [T, T] {
    return items;
}

const pair1 = createPair("a", "b");  // [string, string]
const pair2 = createPair(1, 2);      // [number, number]

7.5 this Parameter and Explicit this Types

Feature Syntax Description Use Case
this Parameter function(this: Type) Explicit type for this context - not a real parameter Method typing, callbacks
this Type this: void Function doesn't use this or requires no specific context Standalone functions
ThisParameterType<T> Extract this parameter type Get the type of this from function type Type utilities
OmitThisParameter<T> Remove this from function type Convert method to standalone function type Function transformations
noImplicitThis Compiler flag Require explicit this types - prevents errors Strict type checking

Example: Explicit this parameter

// this parameter in function
interface User {
    name: string;
    greet(this: User): void;
}

const user: User = {
    name: "Alice",
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

user.greet();  // OK - correct context

// const greetFn = user.greet;
// greetFn();  // Error if noImplicitThis enabled

// this in callbacks
interface Database {
    query(this: Database, sql: string): void;
}

const db: Database = {
    query(sql: string) {
        console.log(`Executing: ${sql}`);
    }
};

// Standalone function with this
function logThis(this: { value: number }): void {
    console.log(this.value);
}

const obj = { value: 42 };
logThis.call(obj);  // 42

// this: void - no this context needed
function calculate(this: void, a: number, b: number): number {
    // Cannot use 'this' here
    return a + b;
}

// Generic this
interface Builder<T> {
    setValue(this: Builder<T>, value: T): this;
    build(this: Builder<T>): T;
}

class StringBuilder implements Builder<string> {
    private value: string = "";
    
    setValue(value: string): this {
        this.value = value;
        return this;
    }
    
    build(): string {
        return this.value;
    }
}

Example: this type utilities

// ThisParameterType - extract this type
type ObjectMethod = (this: { name: string }, age: number) => void;
type ThisType = ThisParameterType<ObjectMethod>;
// Result: { name: string }

// OmitThisParameter - remove this parameter
type StandaloneMethod = OmitThisParameter<ObjectMethod>;
// Result: (age: number) => void

// Practical use case
interface Context {
    prefix: string;
}

function log(this: Context, message: string): void {
    console.log(`${this.prefix}: ${message}`);
}

// Extract this type for type safety
type LogContext = ThisParameterType<typeof log>;

const context: LogContext = { prefix: "INFO" };
log.call(context, "Starting");

// Convert to standalone
type StandaloneLog = OmitThisParameter<typeof log>;
const standaloneFn: StandaloneLog = (message) => {
    console.log(message);
};

// this in class methods
class Component {
    private state: number = 0;
    
    // Method has implicit 'this: Component'
    increment(): void {
        this.state++;
    }
    
    // Bind-safe method with this parameter
    onClick(this: Component, event: Event): void {
        this.increment();
    }
}

7.6 Arrow Functions and Lexical this Binding

Feature Regular Function Arrow Function
this Binding Dynamic - depends on call site Lexical - captured from enclosing scope
arguments Has arguments object No arguments - use rest params
Constructor Can be used with new Cannot be constructor
Syntax function() { } () => { } or () => expr
prototype Has prototype property No prototype

Example: Arrow function this binding

// Arrow functions capture 'this' lexically
class Counter {
    count: number = 0;
    
    // Regular method - 'this' is dynamic
    increment() {
        this.count++;
    }
    
    // Arrow function - 'this' is lexical (captured)
    delayedIncrement = () => {
        setTimeout(() => {
            this.count++;  // 'this' refers to Counter instance
        }, 1000);
    };
    
    // Problem with regular function
    problematicIncrement() {
        setTimeout(function() {
            // this.count++;  // Error: 'this' is undefined or window
        }, 1000);
    }
}

const counter = new Counter();
counter.delayedIncrement();  // Works correctly

// Arrow functions in event handlers
class Button {
    label: string = "Click me";
    
    // Arrow function preserves 'this'
    handleClick = (event: MouseEvent) => {
        console.log(`${this.label} was clicked`);
    };
    
    attachHandler(element: HTMLElement) {
        element.addEventListener("click", this.handleClick);
    }
}

// Array methods with arrow functions
const numbers = [1, 2, 3, 4, 5];

const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);

Example: Arrow function patterns

// Implicit return
const square = (x: number) => x * x;
const greet = (name: string) => `Hello, ${name}!`;

// Object literal return (wrap in parens)
const makePoint = (x: number, y: number) => ({ x, y });

// Generic arrow functions
const identity = <T>(value: T): T => value;
const toArray = <T>(value: T): T[] => [value];

// Arrow function as callback
setTimeout(() => console.log("Done"), 1000);

// Arrow with destructuring
const printUser = ({ name, age }: { name: string; age: number }) => {
    console.log(`${name} is ${age} years old`);
};

// Async arrow function
const fetchUser = async (id: number): Promise<User> => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
};

// Higher-order arrow functions
const multiply = (x: number) => (y: number) => x * y;
const double = multiply(2);
console.log(double(5));  // 10

// Arrow vs regular in class
class Example {
    name = "Example";
    
    // Regular method - shared on prototype
    regularMethod() {
        return this.name;
    }
    
    // Arrow function - unique per instance
    arrowMethod = () => {
        return this.name;
    };
}

// Arrow preserves context in callbacks
class DataLoader {
    data: string[] = [];
    
    load(urls: string[]) {
        // 'this' preserved in arrow function
        urls.forEach(url => {
            fetch(url).then(response => {
                this.data.push(response.url);  // Safe to use 'this'
            });
        });
    }
}
Note: Use arrow functions when you need to preserve this context (event handlers, callbacks). Use regular functions for methods that might be overridden or when you need arguments object.

Function Best Practices

  • Use function type expressions for inline callback types
  • Apply overloading for multiple valid signatures
  • Prefer default parameters over optional with undefined checks
  • Use rest parameters for variable-length argument lists
  • Add explicit this parameter when context matters
  • Use arrow functions for callbacks needing lexical this
  • Enable noImplicitThis for safer this usage

8. Advanced Type Manipulation

8.1 Mapped Types with Key Remapping

Pattern Syntax Description Use Case
Basic Mapped Type {[K in Keys]: Type} Transform each property in a type - iterate over union of keys Make all properties readonly/optional
Key Remapping {[K in Keys as NewK]: Type} Transform property keys during mapping using as clause Prefix/suffix keys, filter properties
Template Remapping as `prefix${K}` Use template literal types to transform key names Add getters/setters prefixes
Conditional Remapping as K extends Cond ? T : F Conditionally include/exclude or transform keys Filter out specific properties
never for Filtering as K extends Bad ? never : K Exclude properties by mapping key to never Remove private/internal properties
Mapped Modifiers +readonly, -? Add (+) or remove (-) readonly/optional modifiers Create mutable/required versions

Example: Basic mapped types and modifiers

// Make all properties optional
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// Make all properties required (remove optional)
type Required<T> = {
    [P in keyof T]-?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

// Remove readonly modifier
type Mutable<T> = {
    -readonly [P in keyof T]: T[P];
};

Example: Key remapping with template literals

// Add getter prefix to all properties
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface User {
    name: string;
    age: number;
}

// Result: { getName: () => string; getAge: () => number; }
type UserGetters = Getters<User>;

// Add setter prefix
type Setters<T> = {
    [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

// Combine getters and setters
type Accessors<T> = Getters<T> & Setters<T>;

Example: Filtering properties with conditional remapping

// Remove properties starting with underscore
type RemovePrivate<T> = {
    [K in keyof T as K extends `_${string}` ? never : K]: T[K];
};

interface Data {
    name: string;
    _id: number;
    _internal: boolean;
    value: number;
}

// Result: { name: string; value: number; }
type PublicData = RemovePrivate<Data>;

// Extract only function properties
type FunctionProps<T> = {
    [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

// Extract only non-function properties
type DataProps<T> = {
    [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

8.2 Template Literal Types and String Manipulation

Type Syntax Description Example
Template Literal `${A}${B}` Combine string literal types - creates union of all combinations `user_${number}`
Uppercase<S> Uppercase<'hello'> Convert string literal to uppercase 'HELLO'
Lowercase<S> Lowercase<'HELLO'> Convert string literal to lowercase 'hello'
Capitalize<S> Capitalize<'hello'> Capitalize first character of string 'Hello'
Uncapitalize<S> Uncapitalize<'Hello'> Lowercase first character of string 'hello'
Pattern Matching S extends `${infer A}${infer B}` Extract parts of string using infer in template Parse string patterns

Example: Creating event name types

// Generate event names from property keys
type EventNames<T> = {
    [K in keyof T as `${string & K}Changed`]: (value: T[K]) => void;
};

interface State {
    count: number;
    name: string;
    active: boolean;
}

// Result: {
//   countChanged: (value: number) => void;
//   nameChanged: (value: string) => void;
//   activeChanged: (value: boolean) => void;
// }
type StateEvents = EventNames<State>;

// HTTP method + path combinations
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIPath = '/users' | '/posts' | '/comments';
type APIRoute = `${HTTPMethod} ${APIPath}`;
// Result: 'GET /users' | 'POST /users' | ... (12 combinations)

Example: String manipulation utilities

// Convert camelCase to kebab-case
type KebabCase<S extends string> = 
    S extends `${infer First}${infer Rest}`
        ? First extends Uppercase<First>
            ? `-${Lowercase<First>}${KebabCase<Rest>}`
            : `${First}${KebabCase<Rest>}`
        : S;

type Result1 = KebabCase<'userName'>; // 'user-name'
type Result2 = KebabCase<'getAPIData'>; // 'get-a-p-i-data'

// Extract email domain
type ExtractDomain<T extends string> = 
    T extends `${string}@${infer Domain}` ? Domain : never;

type Domain = ExtractDomain<'user@example.com'>; // 'example.com'

// Split string by delimiter
type Split<S extends string, D extends string> = 
    S extends `${infer T}${D}${infer U}` 
        ? [T, ...Split<U, D>] 
        : [S];

type Parts = Split<'a.b.c', '.'>; // ['a', 'b', 'c']

8.3 Recursive Types and Recursive Conditional Types

Pattern Description Use Case Limitation
Recursive Type Alias Type that references itself in definition Tree structures, nested objects Max depth ~50 levels
Recursive Conditional Conditional type that recurses on type structure Deep traversal, transformation Compiler depth limit
Tail Recursion Recursion pattern with accumulator parameter Better performance, deeper nesting Still limited by compiler
Structural Recursion Recurse through object/array structure Deep readonly, deep partial Complex nested types

Example: Basic recursive types

// Tree structure
interface TreeNode<T> {
    value: T;
    left?: TreeNode<T>;
    right?: TreeNode<T>;
}

// JSON value representation
type JSONValue = 
    | string 
    | number 
    | boolean 
    | null 
    | JSONValue[] 
    | { [key: string]: JSONValue };

// Linked list
type LinkedList<T> = {
    value: T;
    next: LinkedList<T> | null;
};

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

Example: Deep readonly and partial

// Deep readonly - make all nested properties readonly
type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object
        ? DeepReadonly<T[P]>
        : T[P];
};

interface NestedData {
    user: {
        name: string;
        address: {
            street: string;
            city: string;
        };
    };
}

type ReadonlyData = DeepReadonly<NestedData>;
// All nested properties are readonly

// Deep partial - make all nested properties optional
type DeepPartial<T> = {
    [P in keyof T]?: T[P] extends object
        ? DeepPartial<T[P]>
        : T[P];
};

// Deep required - make all nested properties required
type DeepRequired<T> = {
    [P in keyof T]-?: T[P] extends object
        ? DeepRequired<T[P]>
        : T[P];
};

Example: Path string type for nested objects

// Generate all valid property paths
type Paths<T, Prefix extends string = ''> = {
    [K in keyof T]: K extends string
        ? T[K] extends object
            ? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
            : `${Prefix}${K}`
        : never;
}[keyof T];

interface Data {
    user: {
        name: string;
        address: {
            city: string;
        };
    };
    count: number;
}

// Result: 'user' | 'user.name' | 'user.address' | 'user.address.city' | 'count'
type DataPaths = Paths<Data>;

// Get value type at path
type PathValue<T, P extends string> = 
    P extends `${infer K}.${infer Rest}`
        ? K extends keyof T
            ? PathValue<T[K], Rest>
            : never
        : P extends keyof T
            ? T[P]
            : never;

type CityType = PathValue<Data, 'user.address.city'>; // string

8.4 Higher-Order Types and Type-level Programming

Concept Description Pattern Use Case
Type Constructor Generic type that takes types as parameters type Box<T> = { value: T } Container types, wrappers
Higher-Order Type Type that takes/returns type constructors type Apply<F, T> Generic transformations
Type-level Function Conditional type that computes new type type If<C, T, F> Conditional logic
Type Composition Combine multiple type transformations Compose<F, G, T> Pipeline transformations
Type-level Iteration Build types through recursion/iteration Tuple building, counting Compile-time computation

Example: Type-level utilities

// Identity function at type level
type Identity<T> = T;

// Constant type - always returns same type
type Const<A, B> = A;

// Type-level if/else
type If<Cond extends boolean, Then, Else> = 
    Cond extends true ? Then : Else;

// Type equality check
type Equals<X, Y> = 
    (<T>() => T extends X ? 1 : 2) extends 
    (<T>() => T extends Y ? 1 : 2) ? true : false;

type Test1 = Equals<string, string>; // true
type Test2 = Equals<string, number>; // false

// Type-level AND
type And<A extends boolean, B extends boolean> = 
    A extends true ? (B extends true ? true : false) : false;

// Type-level OR
type Or<A extends boolean, B extends boolean> = 
    A extends true ? true : (B extends true ? true : false);

Example: Tuple and array manipulation

// Tuple length
type Length<T extends any[]> = T['length'];

// Prepend element to tuple
type Prepend<E, T extends any[]> = [E, ...T];

// Append element to tuple
type Append<T extends any[], E> = [...T, E];

// Concat tuples
type Concat<T extends any[], U extends any[]> = [...T, ...U];

// Reverse tuple
type Reverse<T extends any[]> = 
    T extends [infer First, ...infer Rest]
        ? [...Reverse<Rest>, First]
        : [];

type Original = [1, 2, 3];
type Reversed = Reverse<Original>; // [3, 2, 1]

// Build tuple of length N
type Tuple<N extends number, T = unknown, Acc extends T[] = []> = 
    Acc['length'] extends N 
        ? Acc 
        : Tuple<N, T, [T, ...Acc]>;

type FiveItems = Tuple<5, string>; // [string, string, string, string, string]

Example: Type-level arithmetic (simplified)

// Add 1 to tuple length (increment)
type Inc<T extends any[]> = [...T, any];

// Subtract 1 from tuple length (decrement)
type Dec<T extends any[]> = 
    T extends [any, ...infer Rest] ? Rest : [];

// Add two numbers using tuples
type Add<A extends number, B extends number> = 
    [...Tuple<A>, ...Tuple<B>]['length'];

type Result = Add<3, 5>; // 8 (limited by recursion depth)

// Range type - generate union of numbers
type Range<
    From extends number,
    To extends number,
    Acc extends number[] = [],
    Result = never
> = Acc['length'] extends To
    ? Result | To
    : Range<From, To, Inc<Acc>, 
        Acc['length'] extends From 
            ? Result | Acc['length'] 
            : Result>;

type ZeroToFive = Range<0, 5>; // 0 | 1 | 2 | 3 | 4 | 5

8.5 Brand Types and Nominal Typing Patterns

Pattern Technique Purpose Advantage
Brand Property Add unique symbol property Distinguish structurally identical types Type safety for primitives
Phantom Type Generic parameter not used in structure Tag type with metadata No runtime overhead
Opaque Type Hide implementation details Prevent direct construction Encapsulation
Newtype Pattern Wrapper type around primitive Domain-specific types Self-documenting code

Example: Basic branding with symbols

// Brand using unique symbol
declare const UserIdBrand: unique symbol;
type UserId = string & { [UserIdBrand]: true };

declare const ProductIdBrand: unique symbol;
type ProductId = string & { [ProductIdBrand]: true };

// Constructor functions ensure type safety
function createUserId(id: string): UserId {
    // Validation logic here
    return id as UserId;
}

function createProductId(id: string): ProductId {
    return id as ProductId;
}

// Type-safe usage
function getUser(id: UserId) { /* ... */ }
function getProduct(id: ProductId) { /* ... */ }

const userId = createUserId('user-123');
const productId = createProductId('prod-456');

getUser(userId); // ✓ OK
getUser(productId); // ✗ Error: ProductId not assignable to UserId
getUser('user-789'); // ✗ Error: string not assignable to UserId

Example: Branded primitive types

// Email type
type Email = string & { readonly __brand: 'Email' };

function isValidEmail(str: string): str is Email {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}

function sendEmail(to: Email, subject: string) {
    console.log(`Sending to ${to}: ${subject}`);
}

const input = 'user@example.com';
if (isValidEmail(input)) {
    sendEmail(input, 'Hello'); // ✓ Type guard narrows to Email
}
sendEmail(input, 'Hello'); // ✗ Error: string not Email

// Positive integer type
type PositiveInt = number & { readonly __brand: 'PositiveInt' };

function toPositiveInt(n: number): PositiveInt {
    if (!Number.isInteger(n) || n <= 0) {
        throw new Error('Not a positive integer');
    }
    return n as PositiveInt;
}

function setArraySize(size: PositiveInt) { /* ... */ }

Example: Phantom types for state machines

// State machine with phantom types
type State = 'Initial' | 'Loaded' | 'Processed';

interface DataContainer<S extends State> {
    data: unknown;
    _state?: S; // Phantom - not used at runtime
}

// State-specific operations
function load(): DataContainer<'Loaded'> {
    return { data: 'loaded data' };
}

function process(
    container: DataContainer<'Loaded'>
): DataContainer<'Processed'> {
    return { data: 'processed' };
}

function save(container: DataContainer<'Processed'>) {
    console.log('Saving:', container.data);
}

// Type-safe workflow
const initial: DataContainer<'Initial'> = { data: null };
const loaded = load();
const processed = process(loaded);
save(processed); // ✓ OK

save(loaded); // ✗ Error: 'Loaded' not assignable to 'Processed'
process(processed); // ✗ Error: 'Processed' not assignable to 'Loaded'
Note: Brand types enable nominal typing in TypeScript's structural type system. Use them for domain primitives (IDs, emails, URLs), validated data, and state machine transitions. The runtime cost is zero since brands are compile-time only.

8.6 Type-only Imports and Exports

Syntax Purpose Behavior Benefit
import type Import only for type checking Erased during compilation - no runtime code Avoid circular dependencies, smaller bundles
export type Export only type information Type-only export, removed at runtime Clear intent, separate concerns
inline type import import { type T } Mix type and value imports Flexibility, granular control
importsNotUsedAsValues Compiler option for side effects Control import preservation Prevent unwanted side effects

Example: Type-only imports

// types.ts
export interface User {
    id: string;
    name: string;
}

export type UserId = string;

export const DEFAULT_USER: User = {
    id: '0',
    name: 'Guest'
};

// user.ts - type-only import
import type { User, UserId } from './types';
// DEFAULT_USER not imported - no runtime dependency

function processUser(user: User): UserId {
    return user.id;
}

// ✗ Error: Cannot use type import as value
// const guest = DEFAULT_USER;

Example: Inline type imports (TS 4.5+)

// Mixed imports - types and values
import { createUser, type User, type UserId } from './types';
//       ^^^^^^^^^^  ^^^^      ^^^^^^^^^^^^
//       value       type      type

// Can use createUser at runtime
const newUser = createUser();

// Can use User and UserId for typing
function validate(user: User): UserId {
    return user.id;
}

// Re-export with inline type
export { createUser, type User } from './types';

Example: Breaking circular dependencies

// module-a.ts
import type { TypeB } from './module-b';
// Only imports type, not runtime code

export interface TypeA {
    b: TypeB;
    name: string;
}

export function createA(): TypeA { /* ... */ }

// module-b.ts
import type { TypeA } from './module-a';
// Circular reference OK with type-only import

export interface TypeB {
    a: TypeA;
    value: number;
}

export function createB(): TypeB { /* ... */ }

// Without type-only imports, this would cause circular dependency error

Example: Type-only exports

// api-types.ts - pure type definitions
export type { User, Product, Order } from './models';
// Only export types, not any runtime code

// Re-export types from multiple sources
export type {
    RequestHandler,
    Middleware
} from './handlers';

export type {
    Config,
    Environment
} from './config';

// Can be imported in consuming code
import type { User, Product, RequestHandler } from './api-types';
Note: Use import type when you only need types for annotations. This is especially important for:
  • Breaking circular dependencies between modules
  • Importing from libraries where you don't want side effects
  • Reducing bundle size by avoiding unused imports
  • Making dependencies explicit in type-only contexts
Enable "verbatimModuleSyntax": true in tsconfig for stricter type import checking.
Warning: Type-only imports are erased at runtime. Don't use import type for:
  • Classes you need to instantiate
  • Functions you need to call
  • Values you need to reference
  • Modules with required side effects (initialization code)

Advanced Type Manipulation Summary

  • Mapped types transform object properties with key remapping for prefixes, filters, and transformations
  • Template literal types enable string manipulation and pattern matching at the type level
  • Recursive types handle nested structures like trees, JSON, and deep transformations (limited by compiler depth)
  • Higher-order types provide type-level programming with functions, composition, and computation
  • Brand types add nominal typing to distinguish structurally identical types for domain modeling
  • Type-only imports reduce bundle size, break circular deps, and clarify type-only dependencies

9. Type Guards and Runtime Type Checking

9.1 typeof and instanceof Type Guards

Guard Type Syntax Use Case Narrows To
typeof typeof x === 'type' Check primitive types and functions at runtime string, number, boolean, symbol, undefined, object, function
instanceof x instanceof Class Check if object is instance of a class Specific class type
typeof null typeof x === 'object' ⚠️ Returns 'object' for null (JavaScript quirk) Need additional null check
typeof array typeof [] === 'object' ⚠️ Arrays return 'object', use Array.isArray() Use Array.isArray() instead

Example: typeof type guards

function processValue(value: string | number | boolean) {
    if (typeof value === 'string') {
        // Type narrowed to string
        return value.toUpperCase();
    }
    
    if (typeof value === 'number') {
        // Type narrowed to number
        return value.toFixed(2);
    }
    
    // Type narrowed to boolean
    return value ? 'yes' : 'no';
}

// Function type check
function callIfFunction(fn: unknown) {
    if (typeof fn === 'function') {
        // Type narrowed to Function
        fn();
    }
}

// Handle null and object
function processObject(value: unknown) {
    if (typeof value === 'object' && value !== null) {
        // Type narrowed to object (excluding null)
        console.log('Is object:', value);
    }
}

Example: instanceof type guards

class Dog {
    bark() { console.log('Woof!'); }
}

class Cat {
    meow() { console.log('Meow!'); }
}

function makeSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        // Type narrowed to Dog
        animal.bark();
    } else {
        // Type narrowed to Cat
        animal.meow();
    }
}

// Built-in classes
function processDate(value: Date | string) {
    if (value instanceof Date) {
        // Type narrowed to Date
        return value.toISOString();
    }
    // Type narrowed to string
    return new Date(value).toISOString();
}

// Error handling
function handleError(error: unknown) {
    if (error instanceof Error) {
        // Type narrowed to Error
        console.error(error.message, error.stack);
    } else {
        console.error('Unknown error:', error);
    }
}

9.2 User-Defined Type Guards and Predicates

Feature Syntax Description Use Case
Type Predicate x is Type Custom function returning type predicate - tells TS about narrowing Complex validation logic
Return Type function(x): x is T Return type must be boolean with is predicate Type guard function signature
Parameter Name paramName is Type Must match parameter name in function signature Type guard reference
Narrowing Scope After guard call Type narrowed only in if/else blocks after guard check Control flow analysis

Example: Basic type predicates

interface Fish {
    swim: () => void;
}

interface Bird {
    fly: () => void;
}

// User-defined type guard
function isFish(animal: Fish | Bird): animal is Fish {
    return (animal as Fish).swim !== undefined;
}

function move(animal: Fish | Bird) {
    if (isFish(animal)) {
        // Type narrowed to Fish
        animal.swim();
    } else {
        // Type narrowed to Bird
        animal.fly();
    }
}

// String validation guard
function isValidEmail(str: string): str is string {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}

function sendEmail(email: string) {
    if (isValidEmail(email)) {
        // Type still string, but validated
        console.log(`Sending to ${email}`);
    } else {
        throw new Error('Invalid email');
    }
}

Example: Narrowing unknown types

// Check if value is string
function isString(value: unknown): value is string {
    return typeof value === 'string';
}

// Check if value is number
function isNumber(value: unknown): value is number {
    return typeof value === 'number' && !isNaN(value);
}

// Check if value is array
function isArray<T>(value: unknown): value is T[] {
    return Array.isArray(value);
}

// Complex object validation
interface User {
    id: number;
    name: string;
    email: string;
}

function isUser(value: unknown): value is User {
    return (
        typeof value === 'object' &&
        value !== null &&
        'id' in value &&
        'name' in value &&
        'email' in value &&
        typeof (value as any).id === 'number' &&
        typeof (value as any).name === 'string' &&
        typeof (value as any).email === 'string'
    );
}

// Usage
function processData(data: unknown) {
    if (isUser(data)) {
        // Type narrowed to User
        console.log(data.name, data.email);
    }
}

Example: Generic type guards

// Generic array element guard
function isArrayOf<T>(
    value: unknown,
    guard: (item: unknown) => item is T
): value is T[] {
    return Array.isArray(value) && value.every(guard);
}

// Usage with primitives
const data: unknown = [1, 2, 3];
if (isArrayOf(data, isNumber)) {
    // Type narrowed to number[]
    data.forEach(n => console.log(n.toFixed(2)));
}

// Nullable type guard
function isNonNullable<T>(value: T): value is NonNullable<T> {
    return value !== null && value !== undefined;
}

function processValue(value: string | null | undefined) {
    if (isNonNullable(value)) {
        // Type narrowed to string
        return value.toUpperCase();
    }
}

9.3 Assertion Functions and asserts Keyword

Feature Syntax Description Behavior
asserts condition asserts condition Assert boolean condition - throws if false Narrows type in rest of scope
asserts x is Type asserts x is T Assert parameter is specific type - throws if not Type narrowing after call
Return Type void or never Assertion functions return void or throw (never returns) Side effect: type narrowing
Throw Behavior Must throw on failure Function must throw or process.exit on assertion failure Compiler assumes success

Example: Basic assertion functions

// Assert condition is true
function assert(condition: unknown, msg?: string): asserts condition {
    if (!condition) {
        throw new Error(msg || 'Assertion failed');
    }
}

function processValue(value: string | null) {
    assert(value !== null, 'Value cannot be null');
    // After assert, type narrowed to string
    console.log(value.toUpperCase());
}

// Assert type
function assertIsString(value: unknown): asserts value is string {
    if (typeof value !== 'string') {
        throw new Error('Value must be a string');
    }
}

function handleInput(input: unknown) {
    assertIsString(input);
    // After assertion, type narrowed to string
    console.log(input.trim());
}

Example: Non-null assertions

// Assert value is not null or undefined
function assertNonNullable<T>(
    value: T,
    message?: string
): asserts value is NonNullable<T> {
    if (value === null || value === undefined) {
        throw new Error(message || 'Value is null or undefined');
    }
}

function getUser(id: string): User | null {
    // ... database lookup
    return null;
}

function displayUser(id: string) {
    const user = getUser(id);
    assertNonNullable(user, 'User not found');
    // Type narrowed from User | null to User
    console.log(user.name, user.email);
}

// Assert array has elements
function assertNotEmpty<T>(
    array: T[],
    message?: string
): asserts array is [T, ...T[]] {
    if (array.length === 0) {
        throw new Error(message || 'Array is empty');
    }
}

function processItems(items: string[]) {
    assertNotEmpty(items);
    // Type narrowed to [string, ...string[]] (non-empty tuple)
    const first = items[0]; // Type is string, not string | undefined
}

Example: Complex type assertions

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

// Assert object matches interface
function assertIsUser(value: unknown): asserts value is User {
    if (
        typeof value !== 'object' ||
        value === null ||
        typeof (value as any).id !== 'number' ||
        typeof (value as any).name !== 'string' ||
        typeof (value as any).email !== 'string'
    ) {
        throw new Error('Invalid user object');
    }
}

// API response handling
async function fetchUser(id: string): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    const data: unknown = await response.json();
    
    assertIsUser(data);
    // Type narrowed to User after assertion
    return data;
}

// Discriminated union assertion
type Success = { status: 'success'; data: string };
type Failure = { status: 'error'; error: string };
type Result = Success | Failure;

function assertSuccess(result: Result): asserts result is Success {
    if (result.status !== 'success') {
        throw new Error('Expected success result');
    }
}

function handleResult(result: Result) {
    assertSuccess(result);
    // Type narrowed to Success
    console.log(result.data);
}
Note: Use type predicates (x is T) when you want to check and branch. Use assertion functions (asserts x is T) when failure should terminate execution. Assertions are great for preconditions and invariants.

9.4 Discriminated Union Type Guards

Component Description Example Purpose
Discriminant Property Common literal property across union members type: 'success' | 'error' Distinguish union variants
Literal Type Unique literal value per variant 'success', 'error' Enable type narrowing
Switch Statement Pattern match on discriminant switch(value.type) { ... } Exhaustive checking
Exhaustiveness Check Ensure all cases handled const _: never = value; Compile-time completeness

Example: Basic discriminated unions

// Tagged union with discriminant property
type Shape =
    | { kind: 'circle'; radius: number }
    | { kind: 'rectangle'; width: number; height: number }
    | { kind: 'square'; size: number };

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case 'circle':
            // Type narrowed to { kind: 'circle'; radius: number }
            return Math.PI * shape.radius ** 2;
        
        case 'rectangle':
            // Type narrowed to { kind: 'rectangle'; width: number; height: number }
            return shape.width * shape.height;
        
        case 'square':
            // Type narrowed to { kind: 'square'; size: number }
            return shape.size ** 2;
        
        default:
            // Exhaustiveness check
            const _exhaustive: never = shape;
            throw new Error('Unknown shape');
    }
}

// If statements work too
function describe(shape: Shape): string {
    if (shape.kind === 'circle') {
        return `Circle with radius ${shape.radius}`;
    }
    if (shape.kind === 'rectangle') {
        return `Rectangle ${shape.width}x${shape.height}`;
    }
    return `Square ${shape.size}x${shape.size}`;
}

Example: API response handling

// Success/Error discriminated union
type ApiResponse<T> =
    | { status: 'loading' }
    | { status: 'success'; data: T }
    | { status: 'error'; error: string };

function handleResponse<T>(response: ApiResponse<T>) {
    switch (response.status) {
        case 'loading':
            console.log('Loading...');
            break;
        
        case 'success':
            // response.data is accessible and typed as T
            console.log('Data:', response.data);
            break;
        
        case 'error':
            // response.error is accessible and typed as string
            console.error('Error:', response.error);
            break;
    }
}

// Redux-style action types
type Action =
    | { type: 'INCREMENT'; by: number }
    | { type: 'DECREMENT'; by: number }
    | { type: 'RESET' }
    | { type: 'SET'; value: number };

function reducer(state: number, action: Action): number {
    switch (action.type) {
        case 'INCREMENT':
            return state + action.by;
        case 'DECREMENT':
            return state - action.by;
        case 'RESET':
            return 0;
        case 'SET':
            return action.value;
        default:
            // Exhaustiveness check catches missing cases
            const _: never = action;
            return state;
    }
}

Example: Multiple discriminants

// Multiple discriminant properties
type NetworkState =
    | { state: 'idle' }
    | { state: 'loading'; progress: number }
    | { state: 'success'; data: string; cached: boolean }
    | { state: 'error'; error: string; retryable: boolean };

function handleNetworkState(state: NetworkState) {
    // Primary discriminant
    switch (state.state) {
        case 'idle':
            console.log('Idle');
            break;
        
        case 'loading':
            console.log(`Loading: ${state.progress}%`);
            break;
        
        case 'success':
            // Secondary check on cached
            if (state.cached) {
                console.log('From cache:', state.data);
            } else {
                console.log('Fresh data:', state.data);
            }
            break;
        
        case 'error':
            if (state.retryable) {
                console.log('Retrying...', state.error);
            } else {
                console.error('Fatal error:', state.error);
            }
            break;
    }
}

// Nested discriminated unions
type PaymentMethod =
    | { type: 'card'; card: { number: string; cvv: string } }
    | { type: 'paypal'; email: string }
    | { type: 'crypto'; wallet: string; currency: 'BTC' | 'ETH' };

function processPayment(method: PaymentMethod) {
    if (method.type === 'crypto') {
        // Further narrow by currency
        if (method.currency === 'BTC') {
            console.log('Bitcoin payment to', method.wallet);
        } else {
            console.log('Ethereum payment to', method.wallet);
        }
    }
}

9.5 in Operator for Property Existence Checking

Feature Syntax Description Use Case
in Operator 'prop' in obj Check if property exists in object at runtime Narrow union types by properties
Type Narrowing After in check TypeScript narrows to types containing property Discriminate union members
Optional Properties property?: type Property might be undefined but still "in" object Check existence vs value
Inherited Properties Prototype chain in checks own and inherited properties Use hasOwnProperty for own only

Example: Basic in operator narrowing

interface Cat {
    meow: () => void;
    purr: () => void;
}

interface Dog {
    bark: () => void;
    fetch: () => void;
}

type Pet = Cat | Dog;

function interact(pet: Pet) {
    if ('meow' in pet) {
        // Type narrowed to Cat
        pet.meow();
        pet.purr();
    } else {
        // Type narrowed to Dog
        pet.bark();
        pet.fetch();
    }
}

// Multiple property checks
function feedPet(pet: Pet) {
    if ('bark' in pet) {
        // Dog
        console.log('Feeding dog');
    } else if ('meow' in pet) {
        // Cat
        console.log('Feeding cat');
    }
}

Example: Complex union narrowing

type Vehicle =
    | { type: 'car'; wheels: 4; engine: string }
    | { type: 'bike'; wheels: 2; pedals: boolean }
    | { type: 'boat'; propeller: string };

function describe(vehicle: Vehicle) {
    // Check for specific property
    if ('engine' in vehicle) {
        // Narrowed to car
        console.log(`Car with ${vehicle.wheels} wheels and ${vehicle.engine}`);
    } else if ('pedals' in vehicle) {
        // Narrowed to bike
        console.log(`Bike with ${vehicle.pedals ? 'pedals' : 'no pedals'}`);
    } else {
        // Narrowed to boat
        console.log(`Boat with ${vehicle.propeller}`);
    }
}

// API response with optional fields
interface SuccessResponse {
    success: true;
    data: string;
}

interface ErrorResponse {
    success: false;
    error: string;
    code?: number;
}

type Response = SuccessResponse | ErrorResponse;

function handleResponse(response: Response) {
    if ('error' in response) {
        // Narrowed to ErrorResponse
        console.error('Error:', response.error);
        if ('code' in response && response.code) {
            console.error('Error code:', response.code);
        }
    } else {
        // Narrowed to SuccessResponse
        console.log('Data:', response.data);
    }
}

Example: Combining with other guards

// Combining in with typeof
function processValue(value: unknown) {
    if (typeof value === 'object' && value !== null) {
        if ('length' in value) {
            // Likely an array or array-like
            console.log('Length:', (value as any).length);
        }
        
        if ('toString' in value) {
            // Has toString method (most objects)
            console.log('String:', value.toString());
        }
    }
}

// Check method existence
interface Flyable {
    fly: () => void;
}

interface Swimmable {
    swim: () => void;
}

type Creature = Flyable | Swimmable | (Flyable & Swimmable);

function move(creature: Creature) {
    const canFly = 'fly' in creature;
    const canSwim = 'swim' in creature;
    
    if (canFly && canSwim) {
        console.log('Can fly and swim');
        creature.fly();
        creature.swim();
    } else if (canFly) {
        creature.fly();
    } else {
        creature.swim();
    }
}

9.6 Control Flow Analysis and Type Narrowing

Technique Description Example Pattern Reliability
Truthiness Narrowing if(x) narrows from union with null/undefined/false/0/"" if (x) { /* x is truthy */ } Simple but imprecise
Equality Narrowing === and !== narrow to/from specific values if (x === null) { } Precise
Assignment Narrowing Type narrowed after assignment in same scope x = 'string'; /* x is string */ Within scope only
Early Return Return/throw narrows type in rest of function if (!x) return; /* x is truthy */ Very reliable
Switch Fallthrough Type narrowed through switch cases switch(x.type) { case 'a': } Reliable with discriminants
Exception Narrowing throw removes possibility from control flow if (!x) throw Error(); Reliable

Example: Truthiness and equality narrowing

// Truthiness narrowing
function processValue(value: string | null | undefined) {
    if (value) {
        // Type narrowed to string (null and undefined are falsy)
        console.log(value.toUpperCase());
    }
}

// Be careful with 0 and empty string
function processNumber(value: number | null) {
    if (value) {
        // ⚠️ 0 is falsy, so this might not work as expected
        console.log(value.toFixed(2));
    }
    
    // Better: explicit null check
    if (value !== null) {
        // Type narrowed to number (includes 0)
        console.log(value.toFixed(2));
    }
}

// Equality narrowing
function compare(x: string | number, y: string | number) {
    if (x === y) {
        // Both are same type (string or number)
        console.log(x, y);
    }
}

// Discriminant equality
type Result = 
    | { success: true; value: number }
    | { success: false; error: string };

function handle(result: Result) {
    if (result.success === true) {
        // Narrowed to success case
        console.log(result.value);
    } else {
        // Narrowed to error case
        console.log(result.error);
    }
}

Example: Control flow with early returns

// Early return pattern
function processUser(user: User | null | undefined): string {
    // Guard clause
    if (!user) {
        return 'No user';
    }
    
    // After guard, user is definitely User
    if (!user.email) {
        return 'No email';
    }
    
    // user and user.email both exist
    return user.email.toLowerCase();
}

// Multiple guards
function validateInput(input: string | null | undefined): string {
    if (input === null) {
        throw new Error('Input is null');
    }
    
    if (input === undefined) {
        throw new Error('Input is undefined');
    }
    
    // Type narrowed to string
    if (input.length === 0) {
        throw new Error('Input is empty');
    }
    
    // Type is string with length > 0
    return input.trim();
}

// Nested narrowing
function processData(data: unknown): number {
    if (typeof data !== 'object' || data === null) {
        throw new Error('Not an object');
    }
    
    // Type narrowed to object
    if (!('value' in data)) {
        throw new Error('No value property');
    }
    
    const value = (data as any).value;
    
    if (typeof value !== 'number') {
        throw new Error('Value is not a number');
    }
    
    // Type narrowed to number
    return value;
}

Example: Assignment and control flow

// Type narrowing through assignment
function demo() {
    let x: string | number;
    
    x = 'hello';
    // Type narrowed to string
    console.log(x.toUpperCase());
    
    x = 42;
    // Type narrowed to number
    console.log(x.toFixed(2));
}

// Conditional assignment
function processOptional(value?: string) {
    let result: string;
    
    if (value !== undefined) {
        result = value;
        // result is string, value is string
    } else {
        result = 'default';
    }
    
    // result is always string here
    console.log(result.toUpperCase());
}

// Type guards with assignments
function findUser(id: string): User | undefined {
    // ... lookup logic
    return undefined;
}

function displayUser(id: string) {
    const user = findUser(id);
    
    if (!user) {
        console.log('User not found');
        return;
    }
    
    // user is definitely User here
    console.log(user.name);
    
    // Can reassign
    const admin: User | Admin = user;
    if ('permissions' in admin) {
        // Narrowed to Admin
        console.log(admin.permissions);
    }
}

Example: Complex control flow scenarios

// Unreachable code detection
function handleValue(value: string | number) {
    if (typeof value === 'string') {
        return value.toUpperCase();
    }
    
    // Type narrowed to number
    return value.toFixed(2);
    
    // This would be detected as unreachable
    // console.log('Never executed');
}

// Exhaustiveness with control flow
type Status = 'pending' | 'approved' | 'rejected';

function processStatus(status: Status): string {
    if (status === 'pending') {
        return 'Waiting...';
    }
    
    if (status === 'approved') {
        return 'Approved!';
    }
    
    if (status === 'rejected') {
        return 'Rejected!';
    }
    
    // TypeScript knows this is unreachable
    // status has type 'never' here
    const _exhaustive: never = status;
    return _exhaustive;
}

// Type narrowing with logical operators
function process(value: string | null | undefined) {
    // Using || for defaults
    const str = value || 'default';
    // Type is string
    
    // Using ?? for null/undefined only
    const str2 = value ?? 'default';
    // Type is string
    
    // Using && for chaining
    const result = value && value.toUpperCase();
    // Type is string | null | undefined | false
}
Note: TypeScript's control flow analysis tracks type narrowing through:
  • Conditional statements (if/else, switch, ternary)
  • Early exits (return, throw, break, continue)
  • Type guards (typeof, instanceof, in, user-defined)
  • Assignments within the same scope
Enable strictNullChecks for better control flow analysis.
Warning: Control flow analysis doesn't work across:
  • Function boundaries - type guards don't propagate to inner functions
  • Async boundaries - type narrowing lost after await
  • Array methods - callbacks don't maintain outer scope narrowing
  • Object mutations - changing properties doesn't narrow types

Type Guards Summary

  • typeof/instanceof - Built-in guards for primitives and classes
  • Type predicates (x is T) - Custom guards that return boolean
  • Assertion functions (asserts x is T) - Guards that throw on failure
  • Discriminated unions - Use literal property to distinguish variants
  • in operator - Check property existence to narrow unions
  • Control flow - TypeScript tracks narrowing through conditionals and exits

10. Module System and Declaration Files

10.1 ES Module Syntax (import/export)

Syntax Description Example Use Case
Named Export Export specific values by name export const value = 42; Multiple exports from module
Default Export Single main export per module export default class X {} Primary module export
Named Import Import specific named exports import { value } from './mod'; Selective imports
Default Import Import default export import Mod from './mod'; Main module import
Namespace Import Import all exports as object import * as Mod from './mod'; Bundle all exports
Re-export Export from another module export { value } from './mod'; Module aggregation
Side-effect Import Import for side effects only import './init'; Module initialization

Example: Named exports and imports

// math.ts - named exports
export const PI = 3.14159;
export const E = 2.71828;

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

export function multiply(a: number, b: number): number {
    return a * b;
}

export interface MathResult {
    value: number;
    operation: string;
}

// main.ts - named imports
import { PI, E, add, multiply } from './math';

console.log(PI, E);
const sum = add(5, 3);
const product = multiply(4, 2);

// Import with alias
import { add as sum, multiply as times } from './math';

// Import specific items
import { PI } from './math';

// Import type only
import type { MathResult } from './math';

Example: Default exports and mixed exports

// user.ts - default export
export default class User {
    constructor(public name: string) {}
    
    greet() {
        return `Hello, ${this.name}`;
    }
}

// Alternative: export after declaration
class User {
    constructor(public name: string) {}
}
export default User;

// app.ts - import default
import User from './user';
const user = new User('Alice');

// mixed.ts - both default and named exports
export default class Database {
    connect() { /* ... */ }
}

export const DB_VERSION = '1.0';
export function createConnection() { /* ... */ }

// Import both
import Database, { DB_VERSION, createConnection } from './mixed';

// Can rename default import
import DB from './mixed';

Example: Re-exports and barrel exports

// models/user.ts
export interface User {
    id: string;
    name: string;
}

// models/product.ts
export interface Product {
    id: string;
    price: number;
}

// models/index.ts - barrel export
export { User } from './user';
export { Product } from './product';
export * from './order'; // Re-export all

// Or rename during re-export
export { User as UserModel } from './user';

// app.ts - import from barrel
import { User, Product } from './models';

// Re-export with modifications
export { User } from './user';
export type { Product } from './product'; // Type-only re-export
export { Order as PurchaseOrder } from './order';

Example: Namespace imports and dynamic imports

// utils.ts
export const version = '1.0';
export function format(s: string) { return s.toUpperCase(); }
export function parse(s: string) { return s.toLowerCase(); }

// Import entire module as namespace
import * as Utils from './utils';

console.log(Utils.version);
const formatted = Utils.format('hello');

// Dynamic import (returns Promise)
async function loadModule() {
    const utils = await import('./utils');
    console.log(utils.version);
    utils.format('test');
}

// Conditional loading
if (condition) {
    import('./feature').then(module => {
        module.initialize();
    });
}

// Dynamic import with type
type UtilsModule = typeof import('./utils');
const module: UtilsModule = await import('./utils');

10.2 CommonJS Interoperability and require()

Syntax Description ES Module Equivalent Interop Behavior
module.exports CommonJS export object export default Single export object
exports.prop Add named export property export const prop Named export properties
require() Synchronous import import Requires esModuleInterop
__esModule flag Indicates ES module compiled to CJS Compiler generated Better interop detection

Example: CommonJS export patterns

// commonjs.js - CommonJS module
// Pattern 1: exports object
exports.name = 'MyModule';
exports.version = '1.0';
exports.greet = function(name) {
    return `Hello, ${name}`;
};

// Pattern 2: module.exports replacement
module.exports = {
    name: 'MyModule',
    version: '1.0',
    greet(name) {
        return `Hello, ${name}`;
    }
};

// Pattern 3: Single function export
module.exports = function greet(name) {
    return `Hello, ${name}`;
};

// Pattern 4: Class export
class Database {
    connect() { /* ... */ }
}
module.exports = Database;

// Pattern 5: Mixed (avoid - confusing)
exports.helper = () => {};
module.exports.main = () => {}; // Same as exports.main

Example: Importing CommonJS in TypeScript

// Without esModuleInterop (old style)
import * as express from 'express';
const app = express(); // Works

// With esModuleInterop: true (recommended)
import express from 'express';
const app = express(); // Works like CommonJS

// require() in TypeScript (not recommended)
const fs = require('fs'); // Type: any
const fs: typeof import('fs') = require('fs'); // With types

// Import assignment (legacy)
import fs = require('fs');
fs.readFileSync('/path');

// Type-only import for CommonJS types
import type { Express } from 'express';
const app: Express = createExpressApp();

Example: TypeScript compiled to CommonJS

// source.ts - TypeScript source
export const value = 42;
export default class MyClass {}

// Compiled to CommonJS (target: "commonjs")
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.value = void 0;
exports.value = 42;

class MyClass {
}
exports.default = MyClass;

// __esModule flag helps with interop
// TypeScript/Babel use it to detect ES modules

// Import in Node.js
const mod = require('./source');
console.log(mod.value); // 42
console.log(mod.default); // MyClass (with esModuleInterop)

// With esModuleInterop, can use:
const MyClass = require('./source').default;
Note: Enable esModuleInterop and allowSyntheticDefaultImports in tsconfig.json for better CommonJS compatibility. This allows import React from 'react' instead of import * as React from 'react'.

10.3 Module Resolution Strategies (Node, Classic)

Strategy Algorithm Use Case File Extensions
Node (Node10) Mimics Node.js require() resolution Most common, npm packages .ts, .tsx, .d.ts, .js, .jsx
Node16/NodeNext Node.js ESM resolution with package.json Modern Node.js ESM projects Respects "type": "module"
Classic Legacy pre-1.6 resolution Rarely used, backwards compat .ts, .d.ts only
Bundler Bundler-like resolution (Webpack/Vite) SPA applications with bundlers Assumes bundler handles resolution

Example: Node resolution strategy

// For: import { x } from './module';
// TypeScript searches in order:

// 1. Relative import './module'
./module.ts
./module.tsx
./module.d.ts
./module/package.json (check "types" field)
./module/index.ts
./module/index.tsx
./module/index.d.ts

// 2. Non-relative import 'lodash'
// From /root/src/app.ts:

/root/src/node_modules/lodash.ts
/root/src/node_modules/lodash.tsx
/root/src/node_modules/lodash.d.ts
/root/src/node_modules/lodash/package.json ("types" field)
/root/src/node_modules/lodash/index.ts
/root/src/node_modules/lodash/index.d.ts

/root/node_modules/lodash/...
/node_modules/lodash/...

// 3. Check @types
/root/src/node_modules/@types/lodash.d.ts
/root/src/node_modules/@types/lodash/index.d.ts
/root/node_modules/@types/lodash/...
/node_modules/@types/lodash/...

Example: Node16/NodeNext resolution

// package.json configuration
{
    "type": "module", // Use ESM
    "exports": {
        ".": {
            "types": "./dist/index.d.ts",
            "import": "./dist/index.js",
            "require": "./dist/index.cjs"
        },
        "./utils": {
            "types": "./dist/utils.d.ts",
            "import": "./dist/utils.js"
        }
    }
}

// With Node16/NodeNext, must use extensions
// ✗ Error - missing extension
import { x } from './module';

// ✓ Correct - with extension
import { x } from './module.js'; // .js even for .ts files!

// For directories, must use index
import { y } from './utils/index.js';

// Package imports
import pkg from 'package'; // Uses package.json "exports"
import sub from 'package/submodule'; // Must be in exports map

Example: Path mapping and baseUrl

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

// Usage with path mapping
import { Button } from '@components/Button';
import { User } from '@models';
import { format } from '@utils/string';
import config from '@/config';

// Without mapping (relative paths)
import { Button } from '../../components/Button';
import { User } from '../../models/index';
import { format } from '../utils/string';
import config from './config';

// Note: Bundlers need to be configured to understand paths
// Webpack: use tsconfig-paths-webpack-plugin
// Vite: use resolve.alias
Warning: Path mapping in tsconfig.json is compile-time only. For Node.js runtime, use a tool like tsconfig-paths or configure your bundler. Node16/NodeNext resolution doesn't support paths mapping.

10.4 Declaration Files (.d.ts) and Type Declarations

Feature Syntax Purpose Use Case
Declaration File .d.ts Type definitions without implementation Type libraries, JS libraries
declare var/let/const declare const x: T; Declare global variable Global variables from scripts
declare function declare function fn(): T; Declare global function Global functions
declare class declare class C {} Declare class structure Classes from JS
declare namespace declare namespace N {} Group related declarations Library namespaces
declare module declare module 'pkg' {} Module augmentation Add types to external modules

Example: Basic declaration file

// types.d.ts - declaration file

// Declare global variable
declare const VERSION: string;
declare let currentUser: User | null;

// Declare global function
declare function $(selector: string): Element;
declare function fetch(url: string): Promise<Response>;

// Declare class
declare class EventEmitter {
    on(event: string, handler: Function): void;
    emit(event: string, ...args: any[]): void;
}

// Declare interface (no declare needed)
interface User {
    id: string;
    name: string;
}

// Declare type alias
type ID = string | number;

// Declare enum
declare enum Color {
    Red,
    Green,
    Blue
}

// Usage in .ts files (no import needed for global declarations)
console.log(VERSION);
const el = $('div');
const user: User = { id: '1', name: 'Alice' };

Example: Module declaration files

// lodash.d.ts - types for JavaScript library

declare module 'lodash' {
    // Named exports
    export function debounce<T extends Function>(
        func: T,
        wait: number
    ): T;
    
    export function chunk<T>(
        array: T[],
        size: number
    ): T[][];
    
    export function map<T, U>(
        array: T[],
        iteratee: (value: T) => U
    ): U[];
    
    // Default export
    const _: {
        debounce: typeof debounce;
        chunk: typeof chunk;
        map: typeof map;
    };
    
    export default _;
}

// jquery.d.ts - global + module
declare module 'jquery' {
    export = $; // Export assignment for CommonJS
}

declare const $: {
    (selector: string): JQuery;
    ajax(settings: AjaxSettings): void;
};

interface JQuery {
    addClass(className: string): JQuery;
    removeClass(className: string): JQuery;
    html(content: string): JQuery;
}

interface AjaxSettings {
    url: string;
    method?: string;
    data?: any;
}

Example: Generating declaration files

// tsconfig.json
{
    "compilerOptions": {
        "declaration": true,          // Generate .d.ts files
        "declarationMap": true,       // Generate .d.ts.map for navigation
        "emitDeclarationOnly": false, // Emit both .js and .d.ts
        "declarationDir": "./types"   // Output directory for .d.ts
    }
}

// source.ts
export interface User {
    id: string;
    name: string;
}

export function createUser(name: string): User {
    return { id: Math.random().toString(), name };
}

export default class UserManager {
    private users: User[] = [];
    
    add(user: User): void {
        this.users.push(user);
    }
}

// Generated: source.d.ts
export interface User {
    id: string;
    name: string;
}
export declare function createUser(name: string): User;
export default class UserManager {
    private users;
    add(user: User): void;
}

Example: @types packages

// Install type definitions
// npm install --save-dev @types/node
// npm install --save-dev @types/express
// npm install --save-dev @types/react

// @types/node provides Node.js types
import * as fs from 'fs';
import * as path from 'path';

fs.readFileSync('./file.txt'); // Types available

// @types/express provides Express types
import express from 'express';
const app = express();

// Some packages include their own types
// package.json has "types" field
import lodash from 'lodash-es'; // Has built-in types

// Check if types exist: https://www.npmjs.com/~types
// Or search: npm search @types/package-name

// Override @types with local declarations
// Create: node_modules/@types/custom/index.d.ts
// Or configure typeRoots in tsconfig.json
{
    "compilerOptions": {
        "typeRoots": ["./types", "./node_modules/@types"]
    }
}

10.5 Ambient Module Declarations

Pattern Syntax Purpose Scope
Ambient Module declare module 'name' {} Provide types for untyped modules Project-wide
Wildcard Module declare module '*.css' {} Type non-JS imports (CSS, images, etc.) Asset imports
Module Augmentation declare module 'pkg' { export ... } Add types to existing module Extend libraries
Global Augmentation declare global {} Add to global scope from module Global extensions

Example: Ambient module declarations

// types.d.ts - ambient declarations

// Declare module with no types available
declare module 'legacy-lib' {
    export function doSomething(value: any): any;
    export const VERSION: string;
}

// Now can import
import { doSomething, VERSION } from 'legacy-lib';

// Minimal typing for quick fixes
declare module 'no-types-available' {
    const content: any;
    export default content;
}

import lib from 'no-types-available';

// Specific module path pattern
declare module '@company/*/config' {
    interface Config {
        apiKey: string;
        endpoint: string;
    }
    const config: Config;
    export default config;
}

import config from '@company/users/config';
import config2 from '@company/products/config';

Example: Wildcard module declarations for assets

// global.d.ts - type asset imports

// CSS Modules
declare module '*.module.css' {
    const classes: { [key: string]: string };
    export default classes;
}

declare module '*.module.scss' {
    const classes: { [key: string]: string };
    export default classes;
}

// Regular CSS
declare module '*.css' {
    const content: string;
    export default content;
}

// Images
declare module '*.png' {
    const value: string;
    export default value;
}

declare module '*.jpg' {
    const value: string;
    export default value;
}

declare module '*.svg' {
    import { FC, SVGProps } from 'react';
    const content: FC<SVGProps<SVGSVGElement>>;
    export default content;
}

// JSON (if not using resolveJsonModule)
declare module '*.json' {
    const value: any;
    export default value;
}

// Usage in components
import styles from './Component.module.css';
import logo from './logo.png';
import icon from './icon.svg';
import data from './data.json';

const className = styles.container; // Type: string
const imageSrc = logo; // Type: string

Example: Module augmentation

// Extend existing module with new exports
declare module 'express' {
    // Add custom properties to Request
    interface Request {
        user?: {
            id: string;
            name: string;
        };
        startTime?: number;
    }
    
    // Add custom response methods
    interface Response {
        sendSuccess(data: any): void;
        sendError(message: string): void;
    }
}

// Now can use augmented types
import { Request, Response } from 'express';

app.use((req: Request, res: Response, next) => {
    req.startTime = Date.now(); // ✓ Type-safe
    if (req.user) {
        console.log(req.user.name); // ✓ Type-safe
    }
    res.sendSuccess({ ok: true }); // ✓ Type-safe
});

// Augment module from within a module
// my-plugin.ts
import 'express';

declare module 'express' {
    interface Request {
        customField: string;
    }
}

// Available after import
import './my-plugin';
app.get('/', (req, res) => {
    console.log(req.customField); // ✓ Available
});

Example: Global augmentation from module

// extensions.d.ts - module file with global augmentation

// Must have at least one import/export to be a module
export {};

// Augment global scope
declare global {
    // Add to Window interface
    interface Window {
        myApp: {
            version: string;
            config: AppConfig;
        };
        gtag: (command: string, ...args: any[]) => void;
    }
    
    // Add global variable
    var APP_ENV: 'development' | 'production';
    
    // Add to Array prototype
    interface Array<T> {
        firstOrNull(): T | null;
        lastOrNull(): T | null;
    }
    
    // Add to String prototype
    interface String {
        toKebabCase(): string;
        toCamelCase(): string;
    }
}

// Usage anywhere in project
window.myApp.version; // ✓ Type-safe
console.log(APP_ENV); // ✓ Type-safe

const arr = [1, 2, 3];
const first = arr.firstOrNull(); // ✓ Type-safe

const str = 'hello-world';
const kebab = str.toKebabCase(); // ✓ Type-safe

10.6 Triple-Slash Directives and Reference Types

Directive Syntax Purpose Modern Alternative
/// <reference path="" /> Reference file by path Include declaration file Use import statements
/// <reference types="" /> Reference @types package Include type package Use types array in tsconfig
/// <reference lib="" /> Reference built-in lib Include specific lib types Use lib array in tsconfig
/// <reference no-default-lib="true" /> Exclude default lib Custom lib definitions Rarely needed

Example: Triple-slash path references (legacy)

// types.d.ts
interface User {
    id: string;
    name: string;
}

// main.ts - reference other file
/// <reference path="./types.d.ts" />

const user: User = { id: '1', name: 'Alice' };

// Better modern approach: use import
import type { User } from './types';

// Multiple references
/// <reference path="./globals.d.ts" />
/// <reference path="./utils.d.ts" />
/// <reference path="./models.d.ts" />

// Note: Triple-slash directives must be at top of file
// Before any code or imports

Example: Reference types directive

// Include types from @types package
/// <reference types="node" />

// Now have Node.js types without explicit import
const fs: typeof import('fs') = require('fs');
console.log(__dirname); // Global Node.js variable

// Include jQuery types
/// <reference types="jquery" />

$('div').addClass('active'); // jQuery available globally

// In declaration files (.d.ts)
/// <reference types="react" />

declare module 'my-react-library' {
    import { ComponentType } from 'react';
    export const MyComponent: ComponentType<{}>;
}

// Modern approach in tsconfig.json
{
    "compilerOptions": {
        "types": ["node", "jest", "jquery"]
    }
}

Example: Reference lib directive

// Reference specific library versions
/// <reference lib="es2015" />
/// <reference lib="dom" />

// Enables specific APIs
const map = new Map(); // ES2015
const el = document.querySelector('div'); // DOM

// Reference newer libs
/// <reference lib="es2020.promise" />
/// <reference lib="es2021.string" />

// Promise.allSettled (ES2020)
const results = await Promise.allSettled([
    Promise.resolve(1),
    Promise.reject('error')
]);

// String.replaceAll (ES2021)
const replaced = 'hello'.replaceAll('l', 'L');

// Available lib values:
// es5, es2015, es2016, es2017, es2018, es2019, es2020, es2021
// dom, dom.iterable, webworker, scripthost
// es2015.core, es2015.collection, es2015.promise, etc.

// Modern approach in tsconfig.json
{
    "compilerOptions": {
        "lib": ["es2020", "dom", "dom.iterable"]
    }
}

Example: Advanced triple-slash usage

// custom-lib.d.ts - custom library definitions
/// <reference no-default-lib="true" />

// Provide minimal environment (rare use case)
interface Array<T> {
    length: number;
    push(...items: T[]): number;
}

interface String {
    length: number;
    substring(start: number, end?: number): string;
}

// Combined directives
/// <reference lib="es2020" />
/// <reference types="node" />
/// <reference path="./custom.d.ts" />

// Order matters: must be at very top
/// <reference path="./types.d.ts" />
/// <reference types="node" />
import { readFile } from 'fs'; // After directives

// Directive ignored if not at top
import something from './module';
/// <reference types="node" /> // ✗ Ignored - comes after import
Note: Triple-slash directives are legacy features. Modern TypeScript projects should:
  • Use import statements instead of /// <reference path />
  • Configure types array in tsconfig.json instead of /// <reference types />
  • Configure lib array in tsconfig.json instead of /// <reference lib />
Only use triple-slash directives in declaration files (.d.ts) or for global script files.
Warning: Triple-slash directives:
  • Must appear at the very top of the file (before any code)
  • Are only processed in .d.ts files and at project entry points
  • Don't work in module files (files with import/export)
  • Can cause confusion - prefer explicit imports and tsconfig

Module System Summary

  • ES Modules - Use import/export for modern module syntax with named, default, and namespace patterns
  • CommonJS - Enable esModuleInterop for better CJS compatibility with require()/module.exports
  • Module Resolution - Node/Node16 strategies determine how imports are resolved; use path mapping for aliases
  • Declaration Files - .d.ts files provide types without implementation; generate with declaration: true
  • Ambient Modules - Use declare module for typing untyped libraries and wildcard patterns for assets
  • Triple-Slash Directives - Legacy feature for references; prefer imports and tsconfig configuration

11. TypeScript 5+ Modern Features

11.1 const Type Parameters and const Assertions

Feature Syntax Description Benefit
const Type Parameters TS 5.0 <const T> Infer literal types instead of widening to base type Preserve literal types in generics
const Assertion as const Narrow type to literal values, make properties readonly Immutable data structures
const Context const T extends U Apply const-like inference to constrained types Fine-grained literal inference
Readonly Arrays readonly [...] Create readonly arrays and tuples Immutable collections

Example: const type parameters

// Without const type parameter
function makeArray<T>(items: T[]) {
    return items;
}

const arr1 = makeArray(['a', 'b', 'c']);
// Type: string[] (widened)

// With const type parameter (TS 5.0+)
function makeConstArray<const T>(items: T[]) {
    return items;
}

const arr2 = makeConstArray(['a', 'b', 'c']);
// Type: readonly ["a", "b", "c"] (literal types preserved)

// Practical use case
function createConfig<const T extends Record<string, any>>(config: T) {
    return config;
}

const config = createConfig({
    apiUrl: 'https://api.example.com',
    timeout: 5000,
    retries: 3
});
// Type preserves literal values:
// {
//   readonly apiUrl: "https://api.example.com";
//   readonly timeout: 5000;
//   readonly retries: 3;
// }

// Without const, would be:
// {
//   apiUrl: string;
//   timeout: number;
//   retries: number;
// }

Example: const assertions (as const)

// Basic const assertion
const obj1 = { x: 10, y: 20 };
// Type: { x: number; y: number; }

const obj2 = { x: 10, y: 20 } as const;
// Type: { readonly x: 10; readonly y: 20; }

// Array literal
const arr1 = [1, 2, 3];
// Type: number[]

const arr2 = [1, 2, 3] as const;
// Type: readonly [1, 2, 3]

// String literal
const str1 = 'hello';
// Type: string

const str2 = 'hello' as const;
// Type: "hello"

// Nested objects
const routes = {
    home: '/',
    about: '/about',
    contact: '/contact',
    nested: {
        profile: '/profile',
        settings: '/settings'
    }
} as const;
// All properties readonly, all values literal types

type Route = typeof routes.home; // Type: "/"
type NestedRoute = typeof routes.nested.profile; // Type: "/profile"

Example: Practical const patterns

// Enum-like object
const STATUS = {
    PENDING: 'pending',
    APPROVED: 'approved',
    REJECTED: 'rejected'
} as const;

type Status = typeof STATUS[keyof typeof STATUS];
// Type: "pending" | "approved" | "rejected"

function updateStatus(status: Status) {
    // status must be one of the literal values
}

updateStatus(STATUS.PENDING); // ✓ OK
updateStatus('pending'); // ✓ OK
updateStatus('invalid'); // ✗ Error

// Configuration with const
const CONFIG = {
    api: {
        baseUrl: 'https://api.example.com',
        timeout: 5000,
        endpoints: {
            users: '/users',
            posts: '/posts'
        }
    },
    features: {
        darkMode: true,
        notifications: false
    }
} as const;

// Extract types
type ApiEndpoint = typeof CONFIG.api.endpoints[keyof typeof CONFIG.api.endpoints];
// Type: "/users" | "/posts"

// Tuple with const
const point = [10, 20] as const;
// Type: readonly [10, 20]

function distance(p1: readonly [number, number], p2: readonly [number, number]) {
    return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2);
}

distance(point, [30, 40] as const); // ✓ OK

11.2 satisfies Operator for Type Checking TS 4.9

Feature Syntax Description vs Annotation
satisfies value satisfies Type Validate type without widening inference Preserves literal types
Type Annotation value: Type Widen type to annotation Loses literal types
Use Case Type validation + inference Get both type safety and narrow types Best of both worlds

Example: satisfies vs type annotation

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

// Problem with type annotation - loses literal types
const palette1: Record<Colors, string | number[]> = {
    red: '#ff0000',
    green: [0, 255, 0],
    blue: '#0000ff'
};

palette1.red.toUpperCase(); // ✗ Error: string | number[] doesn't have toUpperCase
palette1.green.map(x => x * 2); // ✗ Error: string | number[] doesn't have map

// Solution with satisfies - preserves literal types
const palette2 = {
    red: '#ff0000',
    green: [0, 255, 0],
    blue: '#0000ff'
} satisfies Record<Colors, string | number[]>;

palette2.red.toUpperCase(); // ✓ OK: red is string
palette2.green.map(x => x * 2); // ✓ OK: green is number[]
palette2.yellow = '#ffff00'; // ✗ Error: yellow not in Colors

// satisfies validates structure but keeps narrow types
type RGB = [number, number, number];

const color = [255, 0, 0] satisfies RGB;
// Type: [number, number, number] (tuple, not array)

color[0] = 100; // ✓ OK
color[3] = 50; // ✗ Error: tuple only has 3 elements

Example: Practical satisfies patterns

// Route configuration with validation
type Route = {
    path: string;
    component: string;
    children?: Route[];
};

const routes = [
    {
        path: '/',
        component: 'Home'
    },
    {
        path: '/about',
        component: 'About',
        children: [
            { path: '/about/team', component: 'Team' }
        ]
    }
] satisfies Route[];

// Type is preserved as literal array, not Route[]
const homePath = routes[0].path; // Type: string, not widened
routes[0].path = '/home'; // ✓ Can mutate

// API response schema validation
type ApiResponse<T> = {
    status: 'success' | 'error';
    data?: T;
    error?: string;
};

const response = {
    status: 'success',
    data: { id: 1, name: 'Alice' }
} satisfies ApiResponse<{ id: number; name: string }>;

if (response.status === 'success') {
    // response.data is { id: number; name: string }, not optional
    console.log(response.data.name); // ✓ OK
}

// Configuration with mixed types
type Config = {
    [key: string]: string | number | boolean;
};

const config = {
    host: 'localhost',
    port: 3000,
    ssl: true,
    debug: false
} satisfies Config;

// Preserves specific types
const port: number = config.port; // ✓ OK: port is number
const host: string = config.host; // ✓ OK: host is string

Example: Complex satisfies scenarios

// Event handlers with satisfies
type EventHandlers = {
    [K in string]: (...args: any[]) => void;
};

const handlers = {
    onClick: (e: MouseEvent) => console.log(e.clientX),
    onSubmit: (data: FormData) => console.log(data),
    onError: (error: Error) => console.error(error.message)
} satisfies EventHandlers;

// Type of handlers.onClick is (e: MouseEvent) => void, not (...args: any[]) => void
handlers.onClick(new MouseEvent('click')); // ✓ Type-safe

// Discriminated unions with satisfies
type Action =
    | { type: 'INCREMENT'; by: number }
    | { type: 'DECREMENT'; by: number }
    | { type: 'RESET' };

const actions = [
    { type: 'INCREMENT', by: 1 },
    { type: 'DECREMENT', by: 5 },
    { type: 'RESET' }
] satisfies Action[];

// Type preserved: each action has its specific shape
actions[0].by; // ✓ OK: number
actions[2].by; // ✗ Error: RESET doesn't have 'by'
Note: Use satisfies when you want:
  • Type validation without widening to the annotation type
  • To preserve literal types while ensuring structural compatibility
  • Autocomplete for object properties while maintaining narrow types
  • Both compile-time checking and runtime-friendly types

11.3 using Declaration for Resource Management TS 5.2

Feature Syntax Description Disposal
using Declaration using resource = init(); Auto-dispose resource when scope exits Calls Symbol.dispose
await using await using resource = init(); Async disposal when scope exits Calls Symbol.asyncDispose
Disposable Symbol.dispose Interface for sync resource cleanup Automatic cleanup
AsyncDisposable Symbol.asyncDispose Interface for async resource cleanup Async cleanup

Example: Basic using declaration

// Implement Disposable interface
class FileHandle implements Disposable {
    constructor(private path: string) {
        console.log(`Opening ${path}`);
    }
    
    read() {
        return `Content of ${this.path}`;
    }
    
    [Symbol.dispose]() {
        console.log(`Closing ${this.path}`);
        // Cleanup logic here
    }
}

// Traditional approach - manual cleanup
function readFileOld() {
    const file = new FileHandle('data.txt');
    try {
        return file.read();
    } finally {
        file[Symbol.dispose]();
    }
}

// With using - automatic cleanup
function readFileNew() {
    using file = new FileHandle('data.txt');
    return file.read();
    // file[Symbol.dispose]() called automatically at scope exit
}

// Multiple resources
function processFiles() {
    using file1 = new FileHandle('input.txt');
    using file2 = new FileHandle('output.txt');
    
    // Use both files
    const content = file1.read();
    
    // Both disposed in reverse order (LIFO)
    // file2 disposed first, then file1
}

Example: Async disposal with await using

// Implement AsyncDisposable
class DatabaseConnection implements AsyncDisposable {
    constructor(private connectionString: string) {
        console.log('Connecting...');
    }
    
    async query(sql: string) {
        // Execute query
        return [];
    }
    
    async [Symbol.asyncDispose]() {
        console.log('Disconnecting...');
        // Async cleanup
        await this.closeConnection();
    }
    
    private async closeConnection() {
        // Wait for pending operations
        await new Promise(resolve => setTimeout(resolve, 100));
    }
}

// Async resource management
async function queryDatabase() {
    await using db = new DatabaseConnection('mongodb://localhost');
    
    const results = await db.query('SELECT * FROM users');
    
    // db[Symbol.asyncDispose]() called automatically
    return results;
}

// Multiple async resources
async function processData() {
    await using db = new DatabaseConnection('db1');
    await using cache = new CacheConnection('redis://localhost');
    
    const data = await db.query('SELECT * FROM data');
    await cache.set('key', data);
    
    // Disposed in reverse order: cache, then db
}

Example: Practical resource management patterns

// Lock/Mutex pattern
class Lock implements Disposable {
    private locked = false;
    
    acquire() {
        if (this.locked) throw new Error('Already locked');
        this.locked = true;
    }
    
    [Symbol.dispose]() {
        this.locked = false;
    }
}

function criticalSection() {
    using lock = new Lock();
    lock.acquire();
    
    // Critical code here
    console.log('In critical section');
    
    // Lock automatically released at scope exit
}

// Transaction pattern
class Transaction implements AsyncDisposable {
    private committed = false;
    
    async execute(sql: string) {
        // Execute SQL
    }
    
    async commit() {
        this.committed = true;
        // Commit transaction
    }
    
    async [Symbol.asyncDispose]() {
        if (!this.committed) {
            console.log('Rolling back transaction');
            // Rollback if not committed
        }
    }
}

async function transferMoney(from: string, to: string, amount: number) {
    await using tx = new Transaction();
    
    await tx.execute(`UPDATE accounts SET balance = balance - ${amount} WHERE id = '${from}'`);
    await tx.execute(`UPDATE accounts SET balance = balance + ${amount} WHERE id = '${to}'`);
    
    await tx.commit();
    // Transaction disposed: commit() called, so no rollback
}

// Cleanup multiple resources
class ResourcePool implements Disposable {
    private resources: Disposable[] = [];
    
    add(resource: Disposable) {
        this.resources.push(resource);
    }
    
    [Symbol.dispose]() {
        // Dispose all resources in reverse order
        for (let i = this.resources.length - 1; i >= 0; i--) {
            this.resources[i][Symbol.dispose]();
        }
    }
}
Note: The using declaration implements explicit resource management (similar to C#'s using, Python's with, Java's try-with-resources). Resources are disposed in reverse declaration order (LIFO).

11.4 Decorator Metadata and Reflect API TS 5.0

Feature Status Description Use Case
Decorator Metadata Stage 3 Proposal Attach metadata to decorated declarations Reflection, DI frameworks
Symbol.metadata New symbol Access decorator metadata Runtime reflection
context.metadata Decorator context Store metadata during decoration Framework integration
Reflect Metadata API Library (reflect-metadata) Design-time type reflection DI, validation

Example: Modern decorators with metadata (TS 5.0+)

// Enable in tsconfig.json:
// "experimentalDecorators": false (use new decorators)

// Class decorator with metadata
function Entity(tableName: string) {
    return function<T extends { new(...args: any[]): {} }>(
        target: T,
        context: ClassDecoratorContext
    ) {
        // Store metadata
        context.metadata[Symbol.for('tableName')] = tableName;
        
        return class extends target {
            static tableName = tableName;
        };
    };
}

// Method decorator with metadata
function Route(path: string, method: string) {
    return function(
        target: Function,
        context: ClassMethodDecoratorContext
    ) {
        // Store route metadata
        if (!context.metadata.routes) {
            context.metadata.routes = [];
        }
        (context.metadata.routes as any[]).push({
            path,
            method,
            handler: context.name
        });
    };
}

@Entity('users')
class User {
    @Route('/users', 'GET')
    getUsers() {
        return [];
    }
    
    @Route('/users/:id', 'GET')
    getUser(id: string) {
        return { id };
    }
}

// Access metadata
const metadata = (User as any)[Symbol.metadata];
console.log(metadata[Symbol.for('tableName')]); // 'users'
console.log(metadata.routes); // Array of route configs

Example: Reflect Metadata API (legacy approach)

// npm install reflect-metadata
// Enable in tsconfig.json:
// "experimentalDecorators": true
// "emitDecoratorMetadata": true

import 'reflect-metadata';

// Define metadata keys
const REQUIRED_KEY = Symbol('required');
const VALIDATION_KEY = Symbol('validation');

// Property decorator using Reflect
function Required(target: any, propertyKey: string) {
    Reflect.defineMetadata(REQUIRED_KEY, true, target, propertyKey);
}

function MinLength(length: number) {
    return function(target: any, propertyKey: string) {
        Reflect.defineMetadata(VALIDATION_KEY, { minLength: length }, target, propertyKey);
    };
}

class CreateUserDto {
    @Required
    @MinLength(3)
    username!: string;
    
    @Required
    email!: string;
    
    age?: number;
}

// Validation using metadata
function validate(obj: any): string[] {
    const errors: string[] = [];
    
    for (const key of Object.keys(obj)) {
        // Check required
        const isRequired = Reflect.getMetadata(REQUIRED_KEY, obj, key);
        if (isRequired && !obj[key]) {
            errors.push(`${key} is required`);
        }
        
        // Check validation rules
        const validation = Reflect.getMetadata(VALIDATION_KEY, obj, key);
        if (validation?.minLength && obj[key]?.length < validation.minLength) {
            errors.push(`${key} must be at least ${validation.minLength} characters`);
        }
    }
    
    return errors;
}

const dto = new CreateUserDto();
dto.username = 'ab'; // Too short
const errors = validate(dto); // ['email is required', 'username must be at least 3 characters']

Example: Design-time type reflection

import 'reflect-metadata';

// Automatic type reflection with emitDecoratorMetadata
function Log(target: any, propertyKey: string) {
    // Get design-time type information
    const type = Reflect.getMetadata('design:type', target, propertyKey);
    const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
    const returnType = Reflect.getMetadata('design:returntype', target, propertyKey);
    
    console.log(`${propertyKey} type:`, type?.name);
    console.log(`${propertyKey} params:`, paramTypes?.map((t: any) => t.name));
    console.log(`${propertyKey} return:`, returnType?.name);
}

class Service {
    @Log
    processUser(user: User, id: number): Promise<boolean> {
        return Promise.resolve(true);
    }
}
// Logs: 
// processUser type: Function
// processUser params: ['User', 'Number']
// processUser return: Promise

// Dependency injection with type reflection
const INJECT_KEY = Symbol('inject');

function Injectable() {
    return function<T extends { new(...args: any[]): {} }>(target: T) {
        // Store parameter types for DI
        const paramTypes = Reflect.getMetadata('design:paramtypes', target) || [];
        Reflect.defineMetadata(INJECT_KEY, paramTypes, target);
        return target;
    };
}

@Injectable()
class UserService {
    constructor(private db: DatabaseService, private logger: LoggerService) {}
}

// DI Container
class Container {
    resolve<T>(target: new (...args: any[]) => T): T {
        const params = Reflect.getMetadata(INJECT_KEY, target) || [];
        const instances = params.map((param: any) => this.resolve(param));
        return new target(...instances);
    }
}

const container = new Container();
const userService = container.resolve(UserService);

11.5 Import Attributes and JSON Modules TS 5.3

Feature Syntax Description Use Case
Import Attributes import x from 'mod' with { } Specify module type/attributes on import JSON, CSS modules
JSON Modules import json from './data.json' with { type: 'json' } Import JSON as module Static JSON import
resolveJsonModule tsconfig option Enable JSON module resolution Type-safe JSON imports
Import Assertions assert { type: 'json' } Legacy syntax (pre-TS 5.3) Deprecated

Example: JSON module imports

// tsconfig.json
{
    "compilerOptions": {
        "module": "esnext",
        "resolveJsonModule": true,
        "esModuleInterop": true
    }
}

// data.json
{
    "name": "MyApp",
    "version": "1.0.0",
    "config": {
        "apiUrl": "https://api.example.com",
        "timeout": 5000
    }
}

// Modern syntax (TS 5.3+) with import attributes
import data from './data.json' with { type: 'json' };

console.log(data.name); // ✓ Type-safe: string
console.log(data.config.timeout); // ✓ Type-safe: number

// Type is inferred from JSON structure
type DataType = typeof data;
// {
//   name: string;
//   version: string;
//   config: { apiUrl: string; timeout: number; }
// }

// Legacy syntax (pre-TS 5.3) - still works
import legacyData from './data.json' assert { type: 'json' };

// Without attributes (requires resolveJsonModule)
import simpleData from './data.json';

// Dynamic import with attributes
const dynamicData = await import('./data.json', {
    with: { type: 'json' }
});

Example: CSS and other module types

// CSS Modules with import attributes
import styles from './styles.css' with { type: 'css' };

// In browsers supporting CSS modules
document.adoptedStyleSheets = [styles];

// TypeScript declaration for CSS modules
declare module '*.css' {
    const stylesheet: CSSStyleSheet;
    export default stylesheet;
}

// Web Assembly modules
import wasmModule from './module.wasm' with { type: 'webassembly' };

// Custom module types (requires bundler support)
import config from './config.toml' with { type: 'toml' };
import data from './data.yaml' with { type: 'yaml' };

// Type declarations for custom formats
declare module '*.toml' {
    const content: Record<string, any>;
    export default content;
}

declare module '*.yaml' {
    const content: Record<string, any>;
    export default content;
}

Example: Typed JSON imports

// config.json with strict typing
{
    "database": {
        "host": "localhost",
        "port": 5432,
        "name": "mydb"
    },
    "features": {
        "auth": true,
        "logging": true
    }
}

// Import with type assertion
import configData from './config.json' with { type: 'json' };

// Define expected structure
interface Config {
    database: {
        host: string;
        port: number;
        name: string;
    };
    features: {
        auth: boolean;
        logging: boolean;
    };
}

// Validate at runtime (optional)
function isValidConfig(data: unknown): data is Config {
    return (
        typeof data === 'object' &&
        data !== null &&
        'database' in data &&
        'features' in data
    );
}

// Use config with type safety
const config: Config = configData;
console.log(config.database.port); // ✓ Type: number

// Array of data
import users from './users.json' with { type: 'json' };

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

const typedUsers: User[] = users;
typedUsers.forEach(user => {
    console.log(user.name.toUpperCase()); // ✓ Type-safe
});

11.6 Node16/NodeNext Module Resolution

Feature Description Requirement Behavior
Node16/NodeNext Modern Node.js ESM resolution File extensions required Matches Node.js behavior
package.json "type" Specify module system "module" or "commonjs" Determines .js meaning
Conditional Exports Export different entry points "exports" field import vs require
File Extensions Must include in imports .js for .ts files Mirrors runtime behavior

Example: Node16/NodeNext configuration

// package.json for ESM project
{
    "name": "my-package",
    "version": "1.0.0",
    "type": "module",  // All .js files are ESM
    "exports": {
        ".": {
            "types": "./dist/index.d.ts",
            "import": "./dist/index.js",
            "require": "./dist/index.cjs"
        },
        "./utils": {
            "types": "./dist/utils.d.ts",
            "import": "./dist/utils.js"
        }
    }
}

// tsconfig.json
{
    "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",  // or "NodeNext"
        "moduleResolution": "Node16",  // or "NodeNext"
        "outDir": "./dist",
        "declaration": true
    }
}

// src/utils.ts
export function formatDate(date: Date): string {
    return date.toISOString();
}

// src/index.ts - MUST use .js extension
import { formatDate } from './utils.js';  // ✓ .js even though source is .ts
import { formatDate } from './utils';     // ✗ Error: must include extension

// For directories, use index
import * as utils from './utils/index.js';  // ✓ OK

// Relative imports need extensions
import type { User } from './types.js';  // ✓ OK
import type { User } from './types';     // ✗ Error

Example: Dual package (CJS + ESM)

// package.json for dual package
{
    "name": "dual-package",
    "version": "1.0.0",
    "type": "module",
    "main": "./dist/index.cjs",     // CJS entry point
    "module": "./dist/index.js",    // ESM entry point
    "types": "./dist/index.d.ts",
    "exports": {
        ".": {
            "types": "./dist/index.d.ts",
            "import": "./dist/index.js",    // ESM
            "require": "./dist/index.cjs",  // CJS
            "default": "./dist/index.js"
        }
    },
    "files": ["dist"]
}

// Build with both formats
// tsconfig.json for ESM build
{
    "compilerOptions": {
        "module": "Node16",
        "outDir": "./dist"
    }
}

// Build CJS separately or use bundler
// Output:
// dist/index.js (ESM)
// dist/index.cjs (CJS)
// dist/index.d.ts (types)

// Consumers can use either
// ESM consumer:
import pkg from 'dual-package';

// CJS consumer:
const pkg = require('dual-package');

Example: Subpath exports and patterns

// package.json with detailed exports
{
    "name": "my-lib",
    "type": "module",
    "exports": {
        // Main entry
        ".": {
            "types": "./dist/index.d.ts",
            "import": "./dist/index.js",
            "require": "./dist/index.cjs"
        },
        // Specific subpaths
        "./utils": "./dist/utils.js",
        "./helpers": "./dist/helpers.js",
        
        // Pattern exports (all files in utils/)
        "./utils/*": {
            "types": "./dist/utils/*.d.ts",
            "import": "./dist/utils/*.js"
        },
        
        // Conditional exports
        "./server": {
            "node": "./dist/server.js",
            "default": "./dist/server-browser.js"
        },
        
        // Block access to internal files
        "./internal/*": null,
        
        // Package.json access
        "./package.json": "./package.json"
    }
}

// Usage:
import lib from 'my-lib';              // Main entry
import { util } from 'my-lib/utils';   // Subpath
import { helper } from 'my-lib/utils/string.js';  // Pattern
import server from 'my-lib/server';    // Conditional
import internal from 'my-lib/internal/secret';  // ✗ Error: blocked
Note: Node16/NodeNext resolution is the modern standard for Node.js projects. Key differences from classic Node resolution:
  • File extensions are required in imports
  • Use .js extension even when importing .ts files
  • package.json "type" field determines module system
  • Respects "exports" field for subpath exports
  • No automatic index.js resolution without explicit path
Warning: Node16/NodeNext is stricter than classic resolution:
  • Path mappings (tsconfig paths) are not supported
  • Must use .js extension in imports, not .ts
  • Directory imports require /index.js explicitly
  • Mixing ESM and CJS requires careful package.json configuration

TypeScript 5+ Features Summary

  • const type parameters - Preserve literal types in generic functions with <const T>
  • satisfies operator - Type validation without widening, preserves literal types
  • using declaration - Automatic resource disposal with Symbol.dispose
  • Decorator metadata - Attach metadata to decorators for reflection and DI
  • Import attributes - Specify module type with 'with { type }' syntax
  • Node16/NodeNext - Modern ESM resolution matching Node.js behavior

12. Asynchronous Programming and Promises

12.1 Promise<T> Types and async/await Syntax

Feature Syntax Description Return Type
Promise<T> Promise<string> Generic type representing async operation result T when awaited
async Function async function() {} Function that always returns Promise Promise<ReturnType>
await await promise Wait for Promise to resolve, extract value Unwrapped T from Promise<T>
Promise.resolve Promise.resolve(value) Create resolved Promise Promise<T>
Promise.reject Promise.reject(error) Create rejected Promise Promise<never>
Promise.all Promise.all([p1, p2]) Wait for all Promises, return array Promise<[T1, T2]>
Promise.race Promise.race([p1, p2]) Return first settled Promise Promise<T1 | T2>

Example: Basic Promise types

// Promise type annotation
const promise1: Promise<string> = Promise.resolve('hello');
const promise2: Promise<number> = Promise.resolve(42);

// Promise that might reject
function fetchData(): Promise<string> {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                resolve('Data loaded');
            } else {
                reject(new Error('Failed to load'));
            }
        }, 1000);
    });
}

// Using Promises with then/catch
fetchData()
    .then(data => console.log(data))  // data: string
    .catch(error => console.error(error));  // error: any

// Promise chaining
function getUser(id: string): Promise<User> {
    return fetch(`/api/users/${id}`)
        .then(response => response.json())
        .then(data => data as User);
}

// Promise constructor typing
const customPromise = new Promise<number>((resolve, reject) => {
    resolve(42);  // ✓ OK: number
    resolve('hello');  // ✗ Error: string not assignable to number
});

Example: async/await syntax

// async function automatically wraps return in Promise
async function getName(): Promise<string> {
    return 'Alice';  // Actually returns Promise<string>
}

async function getAge(): Promise<number> {
    return 25;
}

// await unwraps Promise
async function displayUser() {
    const name = await getName();  // Type: string (not Promise<string>)
    const age = await getAge();    // Type: number
    
    console.log(`${name} is ${age} years old`);
}

// Multiple awaits
async function loadData() {
    const users = await fetchUsers();      // User[]
    const posts = await fetchPosts();      // Post[]
    const comments = await fetchComments(); // Comment[]
    
    return { users, posts, comments };
}

// await with non-Promise values (no-op)
async function demo() {
    const x = await 42;  // Type: number (42 wrapped in Promise.resolve)
    const y = await 'hello';  // Type: string
}

Example: Promise combinators

// Promise.all - parallel execution
async function loadMultiple() {
    const [users, posts, settings] = await Promise.all([
        fetchUsers(),    // Promise<User[]>
        fetchPosts(),    // Promise<Post[]>
        fetchSettings()  // Promise<Settings>
    ]);
    // Type: [User[], Post[], Settings]
    
    return { users, posts, settings };
}

// Promise.allSettled - all results (success or failure)
async function tryLoadAll() {
    const results = await Promise.allSettled([
        fetchUsers(),
        fetchPosts(),
        Promise.reject('error')
    ]);
    
    // Type: PromiseSettledResult<User[] | Post[] | never>[]
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            console.log('Success:', result.value);
        } else {
            console.log('Failed:', result.reason);
        }
    });
}

// Promise.race - first to complete
async function timeout<T>(
    promise: Promise<T>,
    ms: number
): Promise<T> {
    const timeoutPromise = new Promise<never>((_, reject) => {
        setTimeout(() => reject(new Error('Timeout')), ms);
    });
    
    return Promise.race([promise, timeoutPromise]);
}

// Promise.any - first successful
async function loadFromMirrors(urls: string[]): Promise<Response> {
    const promises = urls.map(url => fetch(url));
    return Promise.any(promises);  // First successful response
}

12.2 Typing Async Functions and Return Types

Pattern Return Type Description Behavior
async function Promise<T> Always wraps return value in Promise Automatic Promise wrapping
async () => T Promise<T> Arrow function version Same as async function
Return Promise<T> Promise<T> Explicit Promise return (no unwrapping) Not double-wrapped
Return void Promise<void> Async function with no return Returns Promise<undefined>
Throw in async Promise<never> Always rejects Rejected Promise

Example: Async function return types

// Inferred return type: Promise<string>
async function getString() {
    return 'hello';
}

// Explicit return type
async function getNumber(): Promise<number> {
    return 42;
}

// Returning Promise<T> from async (not double-wrapped)
async function getUser(): Promise<User> {
    // Returning Promise<User> doesn't create Promise<Promise<User>>
    return fetch('/api/user').then(r => r.json());
}

// void async function
async function saveData(data: string): Promise<void> {
    await fetch('/api/save', {
        method: 'POST',
        body: data
    });
    // No return statement, returns Promise<void>
}

// Multiple return paths
async function conditional(flag: boolean): Promise<string | number> {
    if (flag) {
        return 'text';   // string
    }
    return 42;          // number
}

// Early return
async function findUser(id: string): Promise<User | null> {
    if (!id) return null;  // Early return
    
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) return null;
    
    return response.json();
}

Example: Generic async functions

// Generic async function
async function fetchJson<T>(url: string): Promise<T> {
    const response = await fetch(url);
    return response.json();
}

// Usage with type parameter
const user = await fetchJson<User>('/api/user');
const posts = await fetchJson<Post[]>('/api/posts');

// Constrained generic
async function retry<T>(
    fn: () => Promise<T>,
    attempts: number
): Promise<T> {
    for (let i = 0; i < attempts; i++) {
        try {
            return await fn();
        } catch (error) {
            if (i === attempts - 1) throw error;
        }
    }
    throw new Error('All attempts failed');
}

// Multiple generic parameters
async function transform<T, U>(
    data: T,
    transformer: (item: T) => Promise<U>
): Promise<U> {
    return await transformer(data);
}

// Async method in class
class UserService {
    async getUser(id: string): Promise<User> {
        const response = await fetch(`/api/users/${id}`);
        return response.json();
    }
    
    async deleteUser(id: string): Promise<void> {
        await fetch(`/api/users/${id}`, { method: 'DELETE' });
    }
}

Example: Async function types

// Function type for async functions
type AsyncFunction<T> = () => Promise<T>;

const loader: AsyncFunction<string> = async () => {
    return 'loaded';
};

// Async function with parameters
type FetchFunction<T> = (id: string) => Promise<T>;

const fetchUser: FetchFunction<User> = async (id) => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
};

// Interface with async methods
interface ApiClient {
    get<T>(url: string): Promise<T>;
    post<T>(url: string, data: any): Promise<T>;
    delete(url: string): Promise<void>;
}

const client: ApiClient = {
    async get(url) {
        const response = await fetch(url);
        return response.json();
    },
    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            body: JSON.stringify(data)
        });
        return response.json();
    },
    async delete(url) {
        await fetch(url, { method: 'DELETE' });
    }
};

12.3 Promise Utility Types (Awaited<T>)

Utility Type Description Example Use Case
Awaited<T> Unwrap Promise type recursively Awaited<Promise<string>> = string Extract Promise result type
ReturnType with async Get function return type ReturnType<typeof fn> Promise<T> for async functions
Awaited with nested Unwrap deeply nested Promises Awaited<Promise<Promise<T>>> Flatten nested Promises
PromiseSettledResult Type for Promise.allSettled fulfilled | rejected Handle settled results

Example: Awaited utility type

// Extract type from Promise
type Result1 = Awaited<Promise<string>>;  // string
type Result2 = Awaited<Promise<number[]>>;  // number[]

// Works with nested Promises
type Nested = Promise<Promise<Promise<User>>>;
type Unwrapped = Awaited<Nested>;  // User

// Extract return type from async function
async function fetchUser(): Promise<User> {
    const response = await fetch('/api/user');
    return response.json();
}

type UserType = Awaited<ReturnType<typeof fetchUser>>;
// Type: User (not Promise<User>)

// With union types
type AsyncData = Promise<string> | Promise<number>;
type UnwrappedData = Awaited<AsyncData>;  // string | number

// Non-Promise types pass through
type NotPromise = Awaited<string>;  // string
type Mixed = Awaited<string | Promise<number>>;  // string | number

Example: Practical Awaited usage

// Type-safe async result handling
async function loadData() {
    return {
        users: await fetchUsers(),
        posts: await fetchPosts(),
        settings: await fetchSettings()
    };
}

type LoadedData = Awaited<ReturnType<typeof loadData>>;
// Type: {
//   users: User[];
//   posts: Post[];
//   settings: Settings;
// }

// Extract types from Promise arrays
type UserPromise = Promise<User>;
type UsersArray = UserPromise[];

type ExtractedUser = Awaited<UserPromise>;  // User
type ExtractedUsers = Awaited<UsersArray[number]>;  // User

// Helper to unwrap all properties
type UnwrapPromises<T> = {
    [K in keyof T]: Awaited<T[K]>;
};

interface AsyncState {
    user: Promise<User>;
    posts: Promise<Post[]>;
    count: number;  // Already not a Promise
}

type SyncState = UnwrapPromises<AsyncState>;
// Type: {
//   user: User;
//   posts: Post[];
//   count: number;
// }

// Type inference with Promise.all
const promises = [
    Promise.resolve('text'),
    Promise.resolve(42),
    Promise.resolve(true)
] as const;

type AllResults = Awaited<typeof Promise.all<typeof promises>>;
// Type: [string, number, boolean]

Example: PromiseSettledResult types

// Type for Promise.allSettled results
type SettledUser = PromiseSettledResult<User>;
// Type: PromiseFulfilledResult<User> | PromiseRejectedResult

// Type-safe handling of settled results
async function loadAllUsers(ids: string[]) {
    const promises = ids.map(id => fetchUser(id));
    const results = await Promise.allSettled(promises);
    
    // Type: PromiseSettledResult<User>[]
    const users: User[] = [];
    const errors: Error[] = [];
    
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            // result.value is User
            users.push(result.value);
        } else {
            // result.reason is any (error)
            errors.push(result.reason);
        }
    });
    
    return { users, errors };
}

// Custom type guard for fulfilled results
function isFulfilled<T>(
    result: PromiseSettledResult<T>
): result is PromiseFulfilledResult<T> {
    return result.status === 'fulfilled';
}

async function processResults() {
    const results = await Promise.allSettled([
        fetchUser('1'),
        fetchUser('2'),
        fetchUser('3')
    ]);
    
    const users = results
        .filter(isFulfilled)
        .map(result => result.value);  // Type: User[]
}

12.4 Error Handling in Async Code

Pattern Syntax Description Best For
try/catch try { await } catch (e) {} Catch errors in async functions Synchronous error handling style
.catch() promise.catch(handler) Handle Promise rejection Promise chains
Error Type catch (e: unknown) Error parameter is unknown in TS Type-safe error handling
Result Type { success: boolean; data?: T; error?: E } Explicit success/failure types Functional error handling

Example: try/catch with async/await

// Basic try/catch
async function loadUser(id: string): Promise<User | null> {
    try {
        const response = await fetch(`/api/users/${id}`);
        
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        
        return await response.json();
    } catch (error) {
        // error is unknown (not any in strict mode)
        console.error('Failed to load user:', error);
        return null;
    }
}

// Type-safe error handling
async function safeLoad(id: string): Promise<User> {
    try {
        return await loadUser(id);
    } catch (error) {
        // Narrow error type
        if (error instanceof Error) {
            console.error(error.message);
        } else {
            console.error('Unknown error', error);
        }
        throw error;  // Re-throw after logging
    }
}

// Multiple try/catch blocks
async function complexOperation() {
    let user: User | null = null;
    let posts: Post[] = [];
    
    try {
        user = await fetchUser('123');
    } catch (error) {
        console.error('User fetch failed:', error);
    }
    
    try {
        posts = await fetchPosts();
    } catch (error) {
        console.error('Posts fetch failed:', error);
    }
    
    return { user, posts };
}

Example: Result type pattern

// Result type for explicit error handling
type Result<T, E = Error> = 
    | { success: true; data: T }
    | { success: false; error: E };

async function fetchUserSafe(id: string): Promise<Result<User>> {
    try {
        const response = await fetch(`/api/users/${id}`);
        
        if (!response.ok) {
            return {
                success: false,
                error: new Error(`HTTP ${response.status}`)
            };
        }
        
        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        return {
            success: false,
            error: error instanceof Error ? error : new Error('Unknown error')
        };
    }
}

// Usage with type narrowing
async function displayUser(id: string) {
    const result = await fetchUserSafe(id);
    
    if (result.success) {
        // result.data is User
        console.log(result.data.name);
    } else {
        // result.error is Error
        console.error(result.error.message);
    }
}

// Either type (functional approach)
type Either<L, R> = 
    | { type: 'left'; value: L }
    | { type: 'right'; value: R };

async function tryFetch<T>(url: string): Promise<Either<Error, T>> {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return { type: 'right', value: data };
    } catch (error) {
        return {
            type: 'left',
            value: error instanceof Error ? error : new Error('Unknown')
        };
    }
}

Example: Error types and custom errors

// Custom error classes
class ApiError extends Error {
    constructor(
        message: string,
        public statusCode: number,
        public response?: any
    ) {
        super(message);
        this.name = 'ApiError';
    }
}

class NetworkError extends Error {
    constructor(message: string) {
        super(message);
        this.name = 'NetworkError';
    }
}

// Type-safe error throwing
async function fetchData(url: string): Promise<any> {
    let response: Response;
    
    try {
        response = await fetch(url);
    } catch (error) {
        throw new NetworkError('Network request failed');
    }
    
    if (!response.ok) {
        throw new ApiError(
            'API request failed',
            response.status,
            await response.text()
        );
    }
    
    return response.json();
}

// Discriminated error handling
async function handleRequest(url: string) {
    try {
        const data = await fetchData(url);
        return data;
    } catch (error) {
        if (error instanceof ApiError) {
            console.error(`API Error ${error.statusCode}:`, error.message);
            if (error.statusCode === 404) {
                return null;
            }
        } else if (error instanceof NetworkError) {
            console.error('Network error:', error.message);
            // Retry logic here
        } else {
            console.error('Unknown error:', error);
        }
        throw error;
    }
}

// Error union type
type FetchError = ApiError | NetworkError | Error;

async function safeFetch(url: string): Promise<Result<any, FetchError>> {
    try {
        const data = await fetchData(url);
        return { success: true, data };
    } catch (error) {
        return {
            success: false,
            error: error as FetchError
        };
    }
}

12.5 Generator Functions and AsyncGenerator Types

Type Syntax Description Use Case
Generator<T, R, N> function* gen() Sync generator - yields values Lazy sequences, iterators
AsyncGenerator<T, R, N> async function* gen() Async generator - yields Promises Streaming data, pagination
yield yield value Produce value from generator Emit next value
yield* yield* iterable Delegate to another generator Compose generators

Example: Generator functions

// Basic generator
function* numberGenerator(): Generator<number, void, unknown> {
    yield 1;
    yield 2;
    yield 3;
}

// Usage
const gen = numberGenerator();
console.log(gen.next().value);  // 1
console.log(gen.next().value);  // 2
console.log(gen.next().value);  // 3
console.log(gen.next().done);   // true

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

// Take first n values
function* take<T>(n: number, iterable: Iterable<T>): Generator<T> {
    let count = 0;
    for (const value of iterable) {
        if (count++ >= n) break;
        yield value;
    }
}

const firstTen = [...take(10, fibonacci())];
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// Generator with return value
function* withReturn(): Generator<number, string, unknown> {
    yield 1;
    yield 2;
    return 'done';  // Return value different from yield type
}

const result = withReturn();
console.log(result.next());  // { value: 1, done: false }
console.log(result.next());  // { value: 2, done: false }
console.log(result.next());  // { value: 'done', done: true }

Example: Async generators

// Async generator for pagination
async function* fetchPages(
    baseUrl: string
): AsyncGenerator<User[], void, unknown> {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();
        
        if (data.users.length === 0) {
            hasMore = false;
        } else {
            yield data.users;
            page++;
        }
    }
}

// Usage with for await...of
async function loadAllUsers() {
    const allUsers: User[] = [];
    
    for await (const users of fetchPages('/api/users')) {
        allUsers.push(...users);
        console.log(`Loaded ${users.length} users`);
    }
    
    return allUsers;
}

// Stream processing
async function* processStream<T, U>(
    source: AsyncIterable<T>,
    transform: (item: T) => Promise<U>
): AsyncGenerator<U> {
    for await (const item of source) {
        yield await transform(item);
    }
}

// Async generator with error handling
async function* fetchWithRetry(
    urls: string[]
): AsyncGenerator<Response, void, unknown> {
    for (const url of urls) {
        let retries = 3;
        while (retries > 0) {
            try {
                const response = await fetch(url);
                yield response;
                break;
            } catch (error) {
                retries--;
                if (retries === 0) throw error;
                await new Promise(resolve => setTimeout(resolve, 1000));
            }
        }
    }
}

Example: Generator type parameters

// Generator<YieldType, ReturnType, NextType>

// NextType - type of value sent via next()
function* counter(): Generator<number, void, number> {
    let count = 0;
    while (true) {
        const increment = yield count;  // increment is number | undefined
        count += increment ?? 1;
    }
}

const gen = counter();
console.log(gen.next());      // { value: 0, done: false }
console.log(gen.next(5));     // { value: 5, done: false }
console.log(gen.next(10));    // { value: 15, done: false }

// Generic async generator
async function* map<T, U>(
    source: AsyncIterable<T>,
    fn: (item: T) => U | Promise<U>
): AsyncGenerator<U, void, unknown> {
    for await (const item of source) {
        yield await fn(item);
    }
}

// Filter async generator
async function* filter<T>(
    source: AsyncIterable<T>,
    predicate: (item: T) => boolean | Promise<boolean>
): AsyncGenerator<T, void, unknown> {
    for await (const item of source) {
        if (await predicate(item)) {
            yield item;
        }
    }
}

// Compose async generators
async function* pipeline<T>(source: AsyncIterable<T>) {
    yield* filter(
        map(source, async (x: T) => transform(x)),
        async (x) => await validate(x)
    );
}

12.6 Callback Types and Event Handler Typing

Pattern Type Description Common Usage
Callback (arg: T) => void Function called with result Node.js style callbacks
Error-first Callback (err: Error | null, data?: T) => void Node.js convention Async operations
Event Handler (event: Event) => void DOM event handler Browser events
Generic Callback <T>(data: T) => void Type-safe callbacks Custom events, streams

Example: Callback function types

// Simple callback
type Callback<T> = (data: T) => void;

function fetchData(url: string, callback: Callback<string>) {
    fetch(url)
        .then(response => response.text())
        .then(data => callback(data));
}

// Error-first callback (Node.js style)
type ErrorCallback<T> = (error: Error | null, data?: T) => void;

function readFile(path: string, callback: ErrorCallback<string>) {
    try {
        // Read file logic
        const data = 'file content';
        callback(null, data);
    } catch (error) {
        callback(error as Error);
    }
}

// Usage
readFile('file.txt', (error, data) => {
    if (error) {
        console.error(error);
        return;
    }
    console.log(data);  // Type: string | undefined
});

// Callback with multiple parameters
type MultiCallback = (success: boolean, data: User, meta: MetaData) => void;

function loadUser(id: string, callback: MultiCallback) {
    // Load user logic
    callback(true, user, metadata);
}

Example: Event handler types

// DOM event handlers
type ClickHandler = (event: MouseEvent) => void;
type KeyHandler = (event: KeyboardEvent) => void;
type FormHandler = (event: FormEvent) => void;

// Button click handler
const handleClick: ClickHandler = (event) => {
    console.log(event.clientX, event.clientY);
    event.preventDefault();
};

// Generic event handler
type EventHandler<T extends Event> = (event: T) => void;

const handleSubmit: EventHandler<FormEvent> = (event) => {
    event.preventDefault();
    const form = event.currentTarget as HTMLFormElement;
    // Process form
};

// React-style event handlers
type ReactChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => void;

const handleChange: ReactChangeHandler = (event) => {
    const value = event.target.value;
    console.log(value);
};

// Custom event emitter
class EventEmitter<T> {
    private listeners: Array<(data: T) => void> = [];
    
    on(listener: (data: T) => void): void {
        this.listeners.push(listener);
    }
    
    emit(data: T): void {
        this.listeners.forEach(listener => listener(data));
    }
}

// Usage
const emitter = new EventEmitter<User>();
emitter.on((user) => {
    console.log(user.name);  // Type-safe: user is User
});

Example: Promisify callbacks

// Convert callback-based function to Promise
function promisify<T>(
    fn: (callback: ErrorCallback<T>) => void
): Promise<T> {
    return new Promise((resolve, reject) => {
        fn((error, data) => {
            if (error) reject(error);
            else resolve(data!);
        });
    });
}

// Generic promisify for Node.js functions
function promisifyNode<T>(
    fn: (...args: any[]) => void
): (...args: any[]) => Promise<T> {
    return function(...args) {
        return new Promise((resolve, reject) => {
            fn(...args, (error: Error | null, data?: T) => {
                if (error) reject(error);
                else resolve(data!);
            });
        });
    };
}

// Usage
function oldStyleRead(path: string, callback: ErrorCallback<string>) {
    // Old callback-based code
    callback(null, 'content');
}

const readAsync = promisify(oldStyleRead);

// Now can use with async/await
async function example() {
    const content = await readAsync('file.txt');
    console.log(content);
}

// Observable-style callbacks
interface Observer<T> {
    next: (value: T) => void;
    error: (error: Error) => void;
    complete: () => void;
}

function subscribe<T>(observer: Observer<T>) {
    try {
        observer.next(value);
        observer.complete();
    } catch (error) {
        observer.error(error as Error);
    }
}
Note: Modern async patterns:
  • Prefer async/await over callbacks for readability
  • Use Promise.all for parallel async operations
  • Consider async generators for streaming data
  • Use Result types for explicit error handling
  • Enable strictNullChecks for better async type safety
Warning: Common async pitfalls:
  • Forgetting await - returns Promise instead of value
  • Error in catch is unknown, not any - requires type narrowing
  • Parallel requests with sequential await - use Promise.all
  • Unhandled Promise rejections - always handle with catch or try/catch
  • Type narrowing doesn't persist across await boundaries

Asynchronous Programming Summary

  • Promise<T> - Generic type for async operations with async/await syntax
  • Async functions - Always return Promise<T>, automatic Promise wrapping
  • Awaited<T> - Utility type to unwrap Promise types recursively
  • Error handling - try/catch with unknown type, Result types for explicit errors
  • Generators - Generator<T> for sync, AsyncGenerator<T> for async iteration
  • Callbacks - Type-safe callbacks with error-first pattern and event handlers

13. Configuration and Compiler Options

13.1 tsconfig.json Configuration File

Property Description Example
compilerOptions Main configuration object for compiler settings { "target": "ES2022", "module": "ESNext" }
include Array of file patterns to include in compilation ["src/**/*", "types/**/*"]
exclude Array of file patterns to exclude from compilation ["node_modules", "dist", "**/*.test.ts"]
files Explicit list of files to include (overrides include) ["src/main.ts", "src/types.d.ts"]
extends Inherit configuration from another tsconfig file "./tsconfig.base.json"
references Array of project references for composite projects [{ "path": "./packages/core" }]
watchOptions Configuration for watch mode file watching { "watchFile": "useFsEvents" }
typeAcquisition Control automatic type acquisition for JS projects { "enable": true, "include": ["jquery"] }

Example: Basic tsconfig.json Structure

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "lib": ["ES2022", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

// Multi-project setup with extends
// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

// tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

// Node.js project configuration
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

13.2 Compiler Options and Strict Mode Settings

Option Description Value/Example
strict Enable all strict type checking options true (enables all below)
strictNullChecks Prevent null/undefined from being assignable to other types true (included in strict)
strictFunctionTypes Check function parameter types contravariantly true (included in strict)
strictBindCallApply Enable strict checking of bind, call, apply methods true (included in strict)
strictPropertyInitialization Ensure class properties are initialized in constructor true (included in strict)
noImplicitAny Error on expressions and declarations with implied any true (included in strict)
noImplicitThis Error when 'this' expression has implied any type true (included in strict)
alwaysStrict Parse in strict mode and emit "use strict" true (included in strict)
noUnusedLocals Report errors on unused local variables true (not in strict)
noUnusedParameters Report errors on unused function parameters true (not in strict)
noImplicitReturns Report error when not all code paths return a value true (not in strict)
noFallthroughCasesInSwitch Report errors for fallthrough cases in switch true (not in strict)
noUncheckedIndexedAccess Include undefined in index signature results true (not in strict)
noImplicitOverride Ensure overriding members are marked with override true (TypeScript 4.3+)
noPropertyAccessFromIndexSignature Require bracket notation for index signature properties true (TypeScript 4.2+)
allowUnusedLabels Allow unused labels in code false (default)
allowUnreachableCode Allow unreachable code without error false (default)
exactOptionalPropertyTypes Differentiate between undefined and missing properties true (TypeScript 4.4+)

Example: Strict Mode Configuration

// Recommended strict configuration
{
  "compilerOptions": {
    // Enable all strict checks
    "strict": true,
    
    // Additional recommended checks not in strict
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "exactOptionalPropertyTypes": true,
    
    // Disallow unreachable code
    "allowUnusedLabels": false,
    "allowUnreachableCode": false
  }
}

// Impact of strictNullChecks
// With strictNullChecks: false
let name: string = null; // OK
let age: number = undefined; // OK

// With strictNullChecks: true
let name: string = null; // Error
let age: number = undefined; // Error
let name2: string | null = null; // OK
let age2: number | undefined = undefined; // OK

// Impact of noUncheckedIndexedAccess
interface Users {
  [id: string]: { name: string; }
}
const users: Users = {};

// With noUncheckedIndexedAccess: false
const user = users["123"]; // Type: { name: string; }
console.log(user.name); // No error, but could crash

// With noUncheckedIndexedAccess: true
const user2 = users["123"]; // Type: { name: string; } | undefined
console.log(user2.name); // Error: Object is possibly undefined
if (user2) {
  console.log(user2.name); // OK
}

13.3 Path Mapping and baseUrl Configuration

Option Description Example
baseUrl Base directory for resolving non-relative module names "./src" or "."
paths Map module names to locations relative to baseUrl { "@/*": ["*"], "@components/*": ["components/*"] }
rootDirs List of root folders whose contents are merged at runtime ["src/", "generated/"]
typeRoots Directories containing type definitions ["./node_modules/@types", "./types"]
types Specific type packages to include (limits auto-inclusion) ["node", "jest", "express"]
moduleResolution Strategy for resolving modules: Node, Node16, NodeNext, Classic "Node16" (recommended for Node.js projects)
resolveJsonModule Allow importing .json files as modules true
allowImportingTsExtensions Allow importing .ts files with extension (TS 5.0+) true

Example: Path Mapping Configuration

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      // Map @ to src root
      "@/*": ["*"],
      
      // Map specific directories
      "@components/*": ["components/*"],
      "@utils/*": ["utils/*"],
      "@services/*": ["services/*"],
      "@types/*": ["types/*"],
      
      // Map to specific files
      "@config": ["config/index.ts"],
      
      // Multiple fallback locations
      "@shared/*": [
        "shared/*",
        "../shared/*",
        "../../packages/shared/*"
      ]
    },
    "typeRoots": [
      "./node_modules/@types",
      "./types"
    ]
  }
}

// Usage with path mapping
// Before: Relative imports
import { Button } from '../../../components/Button';
import { formatDate } from '../../../utils/date';
import config from '../../../config';

// After: Path mapping
import { Button } from '@components/Button';
import { formatDate } from '@utils/date';
import config from '@config';

// Using rootDirs for virtual directory structure
{
  "compilerOptions": {
    "rootDirs": [
      "src",
      "generated"
    ]
  }
}

// File structure:
// src/index.ts
// generated/api-types.ts

// In src/index.ts, can import as if in same directory
import { ApiTypes } from './api-types'; // Resolves to generated/api-types.ts

// Monorepo path mapping
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@myorg/core": ["packages/core/src"],
      "@myorg/utils": ["packages/utils/src"],
      "@myorg/ui": ["packages/ui/src"]
    }
  }
}

// JSON module resolution
{
  "compilerOptions": {
    "resolveJsonModule": true,
    "esModuleInterop": true
  }
}

import packageJson from './package.json';
console.log(packageJson.version); // Fully typed

13.4 Include/Exclude Patterns and File Selection

Pattern Description Example
* Matches zero or more characters (excluding directory separator) "src/*.ts" - all .ts files in src
? Matches exactly one character (excluding directory separator) "file?.ts" - file1.ts, fileA.ts
** Matches any directory nested to any level (recursive) "src/**/*.ts" - all .ts files recursively
include Files/patterns to include in compilation ["src/**/*", "types/**/*"]
exclude Files/patterns to exclude from compilation ["node_modules", "**/*.test.ts"]
files Explicit file list (takes precedence over include/exclude) ["src/main.ts", "src/types.d.ts"]
Default exclude Auto-excluded: node_modules, bower_components, jspm_packages, outDir Implicitly excluded unless in include
include priority include overrides default excludes (except outDir) ["node_modules/my-types/**/*"]

Example: File Selection Patterns

// Basic include/exclude patterns
{
  "include": [
    "src/**/*"  // All files in src directory recursively
  ],
  "exclude": [
    "node_modules",  // All node_modules
    "dist",           // Output directory
    "**/*.spec.ts",  // All test files
    "**/*.test.ts"   // All test files
  ]
}

// Multiple source directories
{
  "include": [
    "src/**/*",
    "lib/**/*",
    "types/**/*.d.ts"
  ],
  "exclude": [
    "src/**/*.test.ts",
    "lib/legacy/**/*"
  ]
}

// Explicit files list (overrides include/exclude)
{
  "files": [
    "src/main.ts",
    "src/types.d.ts",
    "src/global.d.ts"
  ]
  // No include/exclude needed when using files
}

// Complex pattern combinations
{
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "!src/**/*.test.ts",  // Negation pattern
    "!src/**/*.spec.ts"
  ],
  "exclude": [
    "**/*.d.ts",           // Exclude all declaration files
    "node_modules",
    "build",
    "dist",
    "coverage",
    ".vscode",
    ".git"
  ]
}

// Including specific files from node_modules
{
  "include": [
    "src/**/*",
    "node_modules/my-custom-types/**/*"  // Override default exclude
  ],
  "exclude": [
    "node_modules/my-custom-types/test/**/*"
  ]
}

// Monorepo file selection
{
  "include": [
    "packages/*/src/**/*.ts",
    "packages/*/src/**/*.tsx"
  ],
  "exclude": [
    "packages/*/node_modules",
    "packages/*/dist",
    "packages/*/**/*.test.ts"
  ]
}

// Test-specific configuration
// tsconfig.test.json
{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.test.ts",
    "src/**/*.spec.ts",
    "test/**/*"
  ],
  "exclude": []  // Override parent exclude
}

// Build vs Dev configurations
// tsconfig.build.json
{
  "extends": "./tsconfig.json",
  "exclude": [
    "**/*.test.ts",
    "**/*.spec.ts",
    "**/__tests__/**",
    "**/__mocks__/**"
  ]
}

13.5 Project References and Composite Projects

Option Description Example
composite Enable project to be referenced and enable faster builds true (required for project references)
references Array of project references this project depends on [{ "path": "../core" }, { "path": "../utils" }]
declarationMap Generate source maps for .d.ts files (for Go to Definition) true (recommended with composite)
declaration Generate .d.ts files (automatically enabled with composite) true (auto-enabled)
incremental Enable incremental compilation (automatically enabled with composite) true (auto-enabled)
tsBuildInfoFile Specify file to store incremental compilation information "./dist/.tsbuildinfo"
--build flag Build mode for project references (tsc --build or tsc -b) tsc -b
prepend Prepend output of referenced project (for outFile) { "path": "../lib", "prepend": true }

Example: Project References Setup

// Monorepo structure
// /packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  },
  "include": ["src/**/*"]
}

// /packages/utils/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../core" }  // Depends on core
  ],
  "include": ["src/**/*"]
}

// /packages/ui/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx"
  },
  "references": [
    { "path": "../core" },
    { "path": "../utils" }
  ],
  "include": ["src/**/*"]
}

// Root tsconfig.json (solution file)
{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" },
    { "path": "./packages/ui" }
  ]
}

// Build commands
// Build all projects
tsc -b

// Build specific project and dependencies
tsc -b packages/ui

// Clean build artifacts
tsc -b --clean

// Force rebuild
tsc -b --force

// Watch mode for all projects
tsc -b --watch

// Verbose output
tsc -b --verbose

// Dry run
tsc -b --dry

// Using project references in code
// In packages/ui/src/Button.tsx
import { CoreService } from '@myorg/core';  // Reference from core
import { formatDate } from '@myorg/utils';  // Reference from utils

// Benefits of composite projects
// 1. Faster builds - only rebuild changed projects
// 2. Better IDE performance - jump to source instead of .d.ts
// 3. Enforce architectural boundaries
// 4. Parallel builds
// 5. Better error messages

// Build vs Solution configuration
// tsconfig.json (for IDE)
{
  "extends": "./tsconfig.base.json",
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" }
  ]
}

// tsconfig.build.json (for production builds)
{
  "extends": "./tsconfig.base.json",
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" }
  ],
  "compilerOptions": {
    "sourceMap": false,
    "declaration": true
  }
}

13.6 Build Mode and Incremental Compilation

Feature Description Configuration/Command
incremental Save information about project graph for faster rebuilds "incremental": true
tsBuildInfoFile Specify where to save incremental build info "tsBuildInfoFile": "./.tsbuildinfo"
--build (tsc -b) Build mode for project references with dependencies tsc -b or tsc --build
--watch Watch mode for automatic recompilation on file changes tsc --watch or tsc -w
--clean Delete outputs of specified projects (with -b) tsc -b --clean
--force Force rebuild of all projects (with -b) tsc -b --force
--verbose Show detailed build process information tsc -b --verbose
--dry Show what would be built without actually building tsc -b --dry
assumeChangesOnlyAffectDirectDependencies Optimize rebuild by limiting impact analysis "assumeChangesOnlyAffectDirectDependencies": true
watchOptions Configure file watching behavior for watch mode { "watchFile": "useFsEvents" }

Example: Incremental Build Configuration

// Enable incremental compilation
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo",
    "outDir": "./dist"
  }
}

// Watch mode configuration
{
  "compilerOptions": {
    "incremental": true
  },
  "watchOptions": {
    // File watching strategies
    "watchFile": "useFsEvents",  // or "fixedPollingInterval", "dynamicPriorityPolling"
    "watchDirectory": "useFsEvents",  // or "fixedPollingInterval", "dynamicPriorityPolling"
    
    // Fallback polling for file systems that don't support events
    "fallbackPolling": "dynamicPriority",
    
    // Debounce settings
    "synchronousWatchDirectory": true,
    
    // Exclude directories from watching
    "excludeDirectories": [
      "**/node_modules",
      "**/.git",
      "**/dist"
    ],
    
    // Exclude files from watching
    "excludeFiles": [
      "**/.tsbuildinfo",
      "**/package-lock.json"
    ]
  }
}

// Build mode commands for project references
// Build all referenced projects
tsc -b

// Build with dependency tracking
tsc -b packages/app packages/lib

// Watch mode for project references
tsc -b --watch

// Clean build outputs
tsc -b --clean

// Force rebuild all
tsc -b --force

// Verbose build output
tsc -b --verbose

// Dry run (show what would be built)
tsc -b --dry

// Combined flags
tsc -b --clean --verbose
tsc -b --force --watch

// Optimize for large codebases
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./.tsbuildinfo",
    
    // Speed optimizations
    "skipLibCheck": true,  // Skip type checking of .d.ts files
    "skipDefaultLibCheck": true,
    
    // For project references
    "assumeChangesOnlyAffectDirectDependencies": true
  }
}

// Parallel builds with project references
// package.json scripts
{
  "scripts": {
    "build": "tsc -b",
    "build:clean": "tsc -b --clean",
    "build:force": "tsc -b --force",
    "build:watch": "tsc -b --watch",
    "build:verbose": "tsc -b --verbose"
  }
}

// CI/CD optimized build
{
  "compilerOptions": {
    "incremental": false,  // Disable for clean CI builds
    "noEmitOnError": true,
    "skipLibCheck": true
  }
}

// Development vs Production configurations
// tsconfig.dev.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "incremental": true,
    "sourceMap": true,
    "declaration": false
  },
  "watchOptions": {
    "watchFile": "useFsEvents",
    "excludeDirectories": ["node_modules", "dist"]
  }
}

// tsconfig.prod.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "incremental": false,
    "sourceMap": false,
    "declaration": true,
    "removeComments": true
  }
}

// Build performance monitoring
// Use --diagnostics flag
tsc --diagnostics

// Output includes:
// Files:            350
// Lines:            45000
// Nodes:            250000
// Identifiers:      80000
// Symbols:          90000
// Types:            30000
// Memory used:      250MB
// I/O Read time:    0.5s
// Parse time:       2.3s
// Bind time:        1.1s
// Check time:       5.8s
// Emit time:        1.2s
// Total time:       10.9s
Note: Configuration best practices:
  • Use strict: true for all new projects to catch errors early
  • Enable incremental for faster development rebuilds
  • Use project references for monorepos and large codebases
  • Configure path mapping to avoid deep relative imports
  • Use extends to share common configurations
  • Separate dev/prod/test configurations with different tsconfig files
  • Add skipLibCheck to speed up compilation in large projects
  • Use tsc -b for project references with automatic dependency tracking
Warning: Configuration pitfalls:
  • Path mappings in tsconfig don't affect runtime - need bundler/Node.js module resolution
  • composite: true requires declaration: true and specific rootDir/outDir
  • include patterns override default excludes (except outDir)
  • types array limits type package inclusion - may break global types
  • watchOptions can significantly impact CPU usage - tune for your file system
  • strictPropertyInitialization requires constructor initialization or definite assignment
  • noUncheckedIndexedAccess adds undefined to all indexed access - affects existing code
  • Project references must use composite projects - can't mix with regular projects

Configuration and Compiler Options Summary

  • tsconfig.json - Main configuration file with compilerOptions, include, exclude, extends, references
  • Strict mode - Enable with strict: true plus additional checks like noUnusedLocals, noImplicitReturns
  • Path mapping - Use baseUrl and paths for clean imports, typeRoots for custom type locations
  • File selection - Glob patterns with *, ?, ** for include/exclude, files for explicit lists
  • Project references - composite: true enables faster monorepo builds with dependency tracking
  • Incremental builds - Enable incremental: true and use tsc -b for project references with caching

14. Framework Integration Patterns

14.1 React TypeScript Patterns and Component Typing

Component Type Syntax Description Use Case
Function Component React.FC<Props> Function component with props type - includes children implicitly Simple components, hooks-based
Function Component (Direct) (props: Props) => JSX.Element Direct function typing - more explicit, no implicit children Preferred modern approach
Props Interface interface Props { } Define component props with TypeScript interface Type-safe prop passing
Children Prop React.ReactNode Type for React children - elements, strings, numbers, fragments Components accepting children
Event Handlers React.MouseEvent<T> Typed DOM events specific to React onClick, onChange, onSubmit
Refs React.RefObject<T> Typed reference to DOM elements or component instances Direct DOM access, imperative APIs
Hooks useState<T>() Generic hooks with type parameters Typed state management

Example: Function components with props

import React from 'react';

// Props interface
interface ButtonProps {
  text: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary';
}

// Modern approach - direct function typing
function Button({ text, onClick, disabled, variant = 'primary' }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn-${variant}`}
    >
      {text}
    </button>
  );
}

// With children
interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// Generic component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

Example: Event handlers and refs

import React, { useState, useRef } from 'react';

interface FormProps {
  onSubmit: (data: FormData) => void;
}

interface FormData {
  username: string;
  email: string;
}

function Form({ onSubmit }: FormProps) {
  const [formData, setFormData] = useState<FormData>({
    username: '',
    email: ''
  });
  
  // Typed ref
  const inputRef = useRef<HTMLInputElement>(null);
  
  // Typed event handler
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));
  };
  
  // Form submit handler
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    onSubmit(formData);
  };
  
  // Button click handler
  const handleFocus = (e: React.MouseEvent<HTMLButtonElement>) => {
    inputRef.current?.focus();
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        ref={inputRef}
        type="text"
        name="username"
        value={formData.username}
        onChange={handleChange}
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
      />
      <button type="button" onClick={handleFocus}>Focus Input</button>
      <button type="submit">Submit</button>
    </form>
  );
}

Example: Hooks with TypeScript

import { useState, useEffect, useReducer, useContext } from 'react';

// useState with explicit type
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);

// useState with type inference
const [name, setName] = useState(''); // string inferred

// useReducer with types
type State = { count: number };
type Action = { type: 'increment' } | { type: 'decrement' } | { type: 'reset' };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    case 'reset': return { count: 0 };
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </div>
  );
}

// useContext with types
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

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

function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

// Custom hook with generics
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((data: T) => setData(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, [url]);
  
  return { data, loading, error };
}

14.2 Vue.js TypeScript Integration and Composition API

Feature Syntax Description Use Case
defineComponent defineComponent({ }) Define component with TypeScript inference Options API with types
Ref<T> ref<T>(value) Typed reactive reference Reactive primitive values
Reactive<T> reactive<T>(obj) Typed reactive object Reactive complex objects
Computed<T> computed<T>(() => T) Typed computed property Derived reactive state
PropType<T> type: Object as PropType<T> Type assertion for complex props Object/array props
EmitsOptions emits: { eventName: (payload: T) => boolean } Typed event emissions Component events

Example: Vue 3 Composition API with TypeScript

<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue';

// Typed refs
const count = ref<number>(0);
const message = ref<string>('Hello');

// Typed reactive object
interface User {
  name: string;
  email: string;
  age: number;
}

const user = reactive<User>({
  name: 'John',
  email: 'john@example.com',
  age: 30
});

// Computed with type inference
const doubleCount = computed(() => count.value * 2);

// Explicit computed type
const displayName = computed<string>(() => {
  return `${user.name} (${user.age})`;
});

// Watch with types
watch(count, (newVal: number, oldVal: number) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`);
});

// Function with types
function increment(): void {
  count.value++;
}

function updateUser(updates: Partial<User>): void {
  Object.assign(user, updates);
}
</script>

<template>
  <div>
    <p>{{ message }}</p>
    <p>Count: {{ count }} (Double: {{ doubleCount }})</p>
    <p>{{ displayName }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

Example: Props and emits with types

<script setup lang="ts">
import { PropType } from 'vue';

// Define props interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Props with types
const props = defineProps({
  title: {
    type: String,
    required: true
  },
  count: {
    type: Number,
    default: 0
  },
  user: {
    type: Object as PropType<User>,
    required: true
  },
  tags: {
    type: Array as PropType<string[]>,
    default: () => []
  }
});

// Typed emits
const emit = defineEmits<{
  (e: 'update', value: number): void;
  (e: 'delete', id: number): void;
  (e: 'change', user: User): void;
}>();

// Or with validation
const emit = defineEmits({
  update: (value: number) => value >= 0,
  delete: (id: number) => typeof id === 'number',
  change: (user: User) => user.email.includes('@')
});

// Use emits
function handleUpdate() {
  emit('update', props.count + 1);
}

function handleDelete() {
  emit('delete', props.user.id);
}
</script>

14.3 Angular TypeScript Features and Decorators

Decorator Target Description Example
@Component Class Define Angular component with metadata @Component({ selector: 'app-root' })
@Injectable Class Mark class as available for dependency injection @Injectable({ providedIn: 'root' })
@Input Property Define input property for component @Input() name: string;
@Output Property Define output event emitter @Output() change = new EventEmitter();
@ViewChild Property Query for child element or component @ViewChild('input') inputEl!: ElementRef;
@HostListener Method Listen to host element events @HostListener('click')

Example: Angular component with TypeScript

import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';

// Interface for component data
interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-card',
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
      <button (click)="handleEdit()">Edit</button>
      <button (click)="handleDelete()">Delete</button>
    </div>
  `,
  styleUrls: ['./user-card.component.css']
})
export class UserCardComponent implements OnInit {
  // Input with type
  @Input() user!: User;
  @Input() editable: boolean = true;
  
  // Output with typed EventEmitter
  @Output() edit = new EventEmitter<User>();
  @Output() delete = new EventEmitter<number>();
  
  ngOnInit(): void {
    console.log('User card initialized', this.user);
  }
  
  handleEdit(): void {
    if (this.editable) {
      this.edit.emit(this.user);
    }
  }
  
  handleDelete(): void {
    this.delete.emit(this.user.id);
  }
}

Example: Angular service with dependency injection

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';

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

interface ApiResponse<T> {
  data: T;
  status: number;
}

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://api.example.com';
  
  constructor(private http: HttpClient) {}
  
  getUsers(): Observable<User[]> {
    return this.http.get<ApiResponse<User[]>>(`${this.apiUrl}/users`)
      .pipe(
        map(response => response.data),
        catchError(this.handleError)
      );
  }
  
  getUser(id: number): Observable<User> {
    return this.http.get<ApiResponse<User>>(`${this.apiUrl}/users/${id}`)
      .pipe(map(response => response.data));
  }
  
  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<ApiResponse<User>>(`${this.apiUrl}/users`, user)
      .pipe(map(response => response.data));
  }
  
  updateUser(id: number, updates: Partial<User>): Observable<User> {
    return this.http.patch<ApiResponse<User>>(`${this.apiUrl}/users/${id}`, updates)
      .pipe(map(response => response.data));
  }
  
  private handleError(error: any): Observable<never> {
    console.error('API Error:', error);
    throw error;
  }
}

14.4 Node.js TypeScript Development and @types/node

Feature Package/Type Description Use Case
@types/node npm i -D @types/node TypeScript definitions for Node.js built-in modules Core Node.js development
Process NodeJS.Process Type for process object - env, argv, exit, etc. Environment variables, CLI args
Buffer Buffer Binary data handling type File I/O, streams, binary data
EventEmitter EventEmitter Event-driven architecture base class Custom event systems
Stream Readable, Writable, Duplex Stream interface types Data streaming, pipes
Module Resolution "moduleResolution": "node16" Node.js-specific module resolution ESM and CommonJS interop

Example: Node.js modules with TypeScript

import * as fs from 'fs/promises';
import * as path from 'path';
import { EventEmitter } from 'events';
import { createReadStream, createWriteStream } from 'fs';

// File operations
async function readConfig(): Promise<Record<string, any>> {
  const configPath = path.join(__dirname, 'config.json');
  const content = await fs.readFile(configPath, 'utf-8');
  return JSON.parse(content);
}

async function writeLog(message: string): Promise<void> {
  const logPath = path.join(__dirname, 'app.log');
  const timestamp = new Date().toISOString();
  await fs.appendFile(logPath, `[${timestamp}] ${message}\n`);
}

// Environment variables with type safety
interface Env {
  NODE_ENV: 'development' | 'production' | 'test';
  PORT: string;
  DATABASE_URL: string;
  API_KEY: string;
}

function getEnv(): Env {
  return {
    NODE_ENV: (process.env.NODE_ENV as Env['NODE_ENV']) || 'development',
    PORT: process.env.PORT || '3000',
    DATABASE_URL: process.env.DATABASE_URL || '',
    API_KEY: process.env.API_KEY || ''
  };
}

// Streams with types
async function copyFile(source: string, dest: string): Promise<void> {
  return new Promise((resolve, reject) => {
    const readStream = createReadStream(source);
    const writeStream = createWriteStream(dest);
    
    readStream.on('error', reject);
    writeStream.on('error', reject);
    writeStream.on('finish', resolve);
    
    readStream.pipe(writeStream);
  });
}

// EventEmitter with types
interface DataEvents {
  data: (value: string) => void;
  error: (error: Error) => void;
  complete: () => void;
}

class DataProcessor extends EventEmitter {
  on<K extends keyof DataEvents>(event: K, listener: DataEvents[K]): this {
    return super.on(event, listener);
  }
  
  emit<K extends keyof DataEvents>(
    event: K,
    ...args: Parameters<DataEvents[K]>
  ): boolean {
    return super.emit(event, ...args);
  }
  
  process(data: string[]): void {
    data.forEach(item => {
      this.emit('data', item);
    });
    this.emit('complete');
  }
}

14.5 Express.js Type Safety and Middleware Typing

Type Syntax Description Use Case
Request<P, ResBody, ReqBody, Query> Request<ParamsDictionary, any, RequestBody> Typed Express request - params, body, query Type-safe route handlers
Response<ResBody> Response<ResponseBody> Typed response body Type-safe responses
NextFunction NextFunction Middleware next callback Middleware chain
RequestHandler<P, ResBody, ReqBody> RequestHandler<Params, Response, Body> Complete handler type with all generics Reusable typed handlers
ErrorRequestHandler (err, req, res, next) => void Error handling middleware type Global error handlers
Router Router() Express router with types Modular routes

Example: Express routes with TypeScript

import express, { Request, Response, NextFunction, RequestHandler } from 'express';

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

// Interfaces for type safety
interface User {
  id: string;
  name: string;
  email: string;
}

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

interface UpdateUserBody {
  name?: string;
  email?: string;
}

interface UserParams {
  userId: string;
}

interface UserQuery {
  page?: string;
  limit?: string;
}

// Typed route handlers
const getUsers: RequestHandler<{}, User[], {}, UserQuery> = async (req, res) => {
  const page = parseInt(req.query.page || '1');
  const limit = parseInt(req.query.limit || '10');
  
  const users = await fetchUsers(page, limit);
  res.json(users);
};

const getUser: RequestHandler<UserParams, User | { error: string }> = async (req, res) => {
  const { userId } = req.params;
  const user = await findUserById(userId);
  
  if (!user) {
    return res.status(404).json({ error: 'User not found' });
  }
  
  res.json(user);
};

const createUser: RequestHandler<{}, User, CreateUserBody> = async (req, res) => {
  const { name, email, password } = req.body;
  
  const newUser = await createNewUser({ name, email, password });
  res.status(201).json(newUser);
};

const updateUser: RequestHandler<UserParams, User, UpdateUserBody> = async (req, res) => {
  const { userId } = req.params;
  const updates = req.body;
  
  const updatedUser = await updateUserData(userId, updates);
  res.json(updatedUser);
};

// Routes
app.get('/users', getUsers);
app.get('/users/:userId', getUser);
app.post('/users', createUser);
app.patch('/users/:userId', updateUser);

Example: Typed middleware

import { Request, Response, NextFunction, RequestHandler } from 'express';

// Extend Express Request with custom properties
declare global {
  namespace Express {
    interface Request {
      user?: User;
      startTime?: number;
    }
  }
}

interface User {
  id: string;
  email: string;
  role: 'admin' | 'user';
}

// Authentication middleware
const authenticate: RequestHandler = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];
    
    if (!token) {
      return res.status(401).json({ error: 'No token provided' });
    }
    
    const user = await verifyToken(token);
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Authorization middleware
function authorize(...roles: User['role'][]): RequestHandler {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
}

// Logging middleware
const logger: RequestHandler = (req, res, next) => {
  req.startTime = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - (req.startTime || 0);
    console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
  });
  
  next();
};

// Validation middleware
function validateBody<T>(schema: (body: any) => body is T): RequestHandler {
  return (req, res, next) => {
    if (!schema(req.body)) {
      return res.status(400).json({ error: 'Invalid request body' });
    }
    next();
  };
}

// Error handling middleware
const errorHandler: express.ErrorRequestHandler = (err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal server error',
    message: err.message
  });
};

// Usage
app.use(logger);
app.get('/admin', authenticate, authorize('admin'), (req, res) => {
  res.json({ message: 'Admin only' });
});
app.use(errorHandler);

14.6 Next.js TypeScript Configuration and API Routes

Feature Type Description Use Case
GetStaticProps GetStaticProps<Props> Type for static generation function Build-time data fetching
GetServerSideProps GetServerSideProps<Props> Type for server-side rendering function Request-time data fetching
GetStaticPaths GetStaticPaths Type for dynamic route paths generation Dynamic static routes
NextApiRequest NextApiRequest API route request type API endpoints
NextApiResponse NextApiResponse<Data> API route response type Type-safe API responses
NextPage NextPage<Props> Page component type Next.js pages
AppProps AppProps<PageProps> _app.tsx component props type Custom App component

Example: Next.js pages with data fetching

import { GetStaticProps, GetServerSideProps, NextPage } from 'next';

// Page props interface
interface Post {
  id: number;
  title: string;
  content: string;
  author: string;
}

interface HomeProps {
  posts: Post[];
}

// Static generation
export const getStaticProps: GetStaticProps<HomeProps> = async (context) => {
  const res = await fetch('https://api.example.com/posts');
  const posts: Post[] = await res.json();
  
  return {
    props: {
      posts
    },
    revalidate: 60 // Revalidate every 60 seconds
  };
};

// Page component
const Home: NextPage<HomeProps> = ({ posts }) => {
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
          <small>By {post.author}</small>
        </article>
      ))}
    </div>
  );
};

export default Home;

// Dynamic route with params
interface PostPageProps {
  post: Post;
}

export const getServerSideProps: GetServerSideProps<PostPageProps> = async (context) => {
  const { id } = context.params as { id: string };
  
  const res = await fetch(`https://api.example.com/posts/${id}`);
  const post: Post = await res.json();
  
  return {
    props: {
      post
    }
  };
};

Example: Next.js API routes

import type { NextApiRequest, NextApiResponse } from 'next';

// Response types
interface User {
  id: number;
  name: string;
  email: string;
}

interface ErrorResponse {
  error: string;
  details?: string;
}

type UserResponse = User | ErrorResponse;

// GET /api/users/[id]
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse<UserResponse>
) {
  const { id } = req.query;
  
  if (req.method !== 'GET') {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  
  try {
    const user = await fetchUser(Number(id));
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    res.status(200).json(user);
  } catch (error) {
    res.status(500).json({
      error: 'Internal server error',
      details: error instanceof Error ? error.message : 'Unknown error'
    });
  }
}

// POST /api/users - Create user
interface CreateUserBody {
  name: string;
  email: string;
}

export default async function createUserHandler(
  req: NextApiRequest,
  res: NextApiResponse<User | ErrorResponse>
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }
  
  const { name, email } = req.body as CreateUserBody;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }
  
  try {
    const newUser = await createUser({ name, email });
    res.status(201).json(newUser);
  } catch (error) {
    res.status(500).json({ error: 'Failed to create user' });
  }
}

// Middleware pattern for API routes
type ApiHandler<T = any> = (
  req: NextApiRequest,
  res: NextApiResponse<T>
) => Promise<void> | void;

function withAuth<T>(handler: ApiHandler<T>): ApiHandler<T | ErrorResponse> {
  return async (req, res) => {
    const token = req.headers.authorization;
    
    if (!token) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    
    try {
      await verifyToken(token);
      return handler(req, res);
    } catch (error) {
      return res.status(401).json({ error: 'Invalid token' });
    }
  };
}

// Protected route
export default withAuth<User>(async (req, res) => {
  const user = await getCurrentUser(req);
  res.status(200).json(user);
});
Note: Framework integration best practices:
  • React - Use direct function typing over React.FC, properly type hooks and event handlers
  • Vue 3 - Leverage <script setup lang="ts"> with Composition API for best inference
  • Angular - Enable strict mode, use interfaces for all data models, leverage RxJS types
  • Node.js - Install @types/node, use proper module resolution (node16/nodenext)
  • Express - Extend Express namespace for custom properties, type all middleware
  • Next.js - Use proper data fetching types, type API routes with NextApiRequest/Response

Framework Integration Summary

  • React - Type props, events, hooks, and refs with React.* types and generics
  • Vue 3 - Use defineProps, defineEmits, ref, reactive with proper type annotations
  • Angular - Leverage decorators (@Component, @Injectable) with typed services and RxJS
  • Node.js - Install @types/node for built-in modules, use proper async/stream types
  • Express - Type Request<P, ResBody, ReqBody, Query> and middleware with generics
  • Next.js - Use GetStaticProps, GetServerSideProps, and NextApiRequest types for data fetching and API routes

15. Testing and Quality Assurance

15.1 Jest TypeScript Configuration and Type Testing

Configuration Package/Setting Description Use Case
ts-jest npm i -D ts-jest @types/jest TypeScript preprocessor for Jest - compiles TS tests Jest with TypeScript support
preset preset: 'ts-jest' Pre-configured Jest setup for TypeScript Quick Jest TS configuration
testEnvironment 'node' | 'jsdom' Test execution environment - Node.js or browser Backend vs frontend tests
globals globals: { 'ts-jest': {...} } ts-jest specific configuration options TypeScript compiler tweaks
@testing-library @testing-library/react Testing utilities with full TypeScript support Component testing
Type Assertions expect(value).toBe<Type> Type-aware Jest matchers Runtime + type checking

Example: Jest configuration for TypeScript

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  transform: {
    "^.+\\.ts$": "ts-jest",
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts'
  ],
  globals: {
    'ts-jest': {
      tsconfig: {
        esModuleInterop: true,
        allowSyntheticDefaultImports: true
      }
    }
  }
};

// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "devDependencies": {
    "@types/jest": "^29.5.0",
    "jest": "^29.5.0",
    "ts-jest": "^29.1.0",
    "typescript": "^5.0.0"
  }
}

Example: Writing typed tests with Jest

import { sum, fetchUser, User } from './math';

describe('sum function', () => {
  it('should add two numbers', () => {
    const result: number = sum(2, 3);
    expect(result).toBe(5);
  });
  
  it('should handle negative numbers', () => {
    expect(sum(-1, 1)).toBe(0);
  });
});

describe('fetchUser function', () => {
  it('should return a user object', async () => {
    const user: User = await fetchUser(1);
    
    expect(user).toHaveProperty('id');
    expect(user).toHaveProperty('name');
    expect(user).toHaveProperty('email');
    expect(typeof user.id).toBe('number');
    expect(typeof user.name).toBe('string');
  });
  
  it('should throw error for invalid id', async () => {
    await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID');
  });
});

// Testing generics
function identity<T>(value: T): T {
  return value;
}

describe('identity function', () => {
  it('should preserve type for string', () => {
    const result: string = identity('hello');
    expect(result).toBe('hello');
  });
  
  it('should preserve type for number', () => {
    const result: number = identity(42);
    expect(result).toBe(42);
  });
  
  it('should preserve type for object', () => {
    const obj = { name: 'John', age: 30 };
    const result: typeof obj = identity(obj);
    expect(result).toEqual(obj);
  });
});

15.2 Type-only Tests and expect-type Library

Library/Feature Syntax Description Use Case
expect-type npm i -D expect-type Compile-time type assertions - no runtime code Type-only testing
expectTypeOf expectTypeOf(val).toEqualTypeOf<T>() Assert exact type match Verify precise types
toMatchTypeOf expectTypeOf(val).toMatchTypeOf<T>() Assert type is assignable to target Check type compatibility
tsd npm i -D tsd Test TypeScript type definitions - .d.ts files Library type definitions
dtslint npm i -D dtslint DefinitelyTyped testing tool @types packages
@ts-expect-error // @ts-expect-error Assert that next line has type error Negative type tests

Example: Type-only tests with expect-type

import { expectTypeOf } from 'expect-type';

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

expectTypeOf(add).returns.toEqualTypeOf<number>();
expectTypeOf(add).parameter(0).toEqualTypeOf<number>();
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>();

// Test generic types
function identity<T>(value: T): T {
  return value;
}

expectTypeOf(identity<string>).returns.toEqualTypeOf<string>();
expectTypeOf(identity<number>).returns.toEqualTypeOf<number>();

// Test type utilities
type User = { id: number; name: string; email: string };
type UserUpdate = Partial<User>;

expectTypeOf<UserUpdate>().toMatchTypeOf<{ id?: number }>();
expectTypeOf<UserUpdate>().toMatchTypeOf<{ name?: string }>();

// Test object types
interface Product {
  id: number;
  name: string;
  price: number;
}

const product: Product = { id: 1, name: 'Widget', price: 9.99 };

expectTypeOf(product).toEqualTypeOf<Product>();
expectTypeOf(product).toHaveProperty('id');
expectTypeOf(product.id).toEqualTypeOf<number>();
expectTypeOf(product.name).toEqualTypeOf<string>();

// Test union and intersection types
type StringOrNumber = string | number;
type NameAndAge = { name: string } & { age: number };

expectTypeOf<StringOrNumber>().toMatchTypeOf<string>();
expectTypeOf<StringOrNumber>().toMatchTypeOf<number>();
expectTypeOf<NameAndAge>().toHaveProperty('name');
expectTypeOf<NameAndAge>().toHaveProperty('age');

Example: Negative type tests

// Test that code produces type errors
function requiresNumber(n: number): void {}

// @ts-expect-error - should fail with string argument
requiresNumber('hello');

// @ts-expect-error - should fail with undefined
requiresNumber(undefined);

// This should compile fine (no error)
requiresNumber(42);

// Test type narrowing
function process(value: string | number) {
  if (typeof value === 'string') {
    // @ts-expect-error - toFixed doesn't exist on string
    value.toFixed(2);
    
    // This is fine
    value.toUpperCase();
  }
}

// Test readonly enforcement
interface ReadonlyConfig {
  readonly apiKey: string;
}

const config: ReadonlyConfig = { apiKey: 'secret' };

// @ts-expect-error - cannot assign to readonly property
config.apiKey = 'new-secret';

// Using expect-type for negative tests
import { expectTypeOf } from 'expect-type';

expectTypeOf<string>().not.toEqualTypeOf<number>();
expectTypeOf<{ a: string }>().not.toMatchTypeOf<{ b: number }>();

// Test that types are NOT assignable
type Admin = { role: 'admin'; permissions: string[] };
type User = { role: 'user'; name: string };

expectTypeOf<Admin>().not.toMatchTypeOf<User>();
expectTypeOf<User>().not.toMatchTypeOf<Admin>();

15.3 Mock Typing and Stub Generation

Feature Syntax Description Use Case
jest.fn() jest.fn<ReturnType, Args>() Create typed mock function Mock functions with types
jest.Mock<T> Mock<ReturnType, Args> Type for Jest mock function Type mock variables
jest.mocked() jest.mocked(fn, deep?) Type helper for mocked modules Type module mocks
Partial<T> Mocks Partial<Interface> Create partial object mocks Mock complex objects
jest.spyOn() jest.spyOn<T, K>(obj, method) Create typed spy on method Monitor method calls
ts-mockito npm i -D ts-mockito Type-safe mocking library Alternative to Jest mocks

Example: Typed mocks with Jest

import { jest } from '@jest/globals';

// Mock typed functions
type FetchUser = (id: number) => Promise<User>;

const mockFetchUser = jest.fn<Promise<User>, [number]>();

mockFetchUser.mockResolvedValue({
  id: 1,
  name: 'John',
  email: 'john@example.com'
});

// Use in tests
const user = await mockFetchUser(1);
expect(mockFetchUser).toHaveBeenCalledWith(1);
expect(user.name).toBe('John');

// Mock class methods
class UserService {
  async getUser(id: number): Promise<User> {
    // implementation
  }
  
  async updateUser(id: number, data: Partial<User>): Promise<User> {
    // implementation
  }
}

const mockUserService = {
  getUser: jest.fn<Promise<User>, [number]>(),
  updateUser: jest.fn<Promise<User>, [number, Partial<User>]>()
};

mockUserService.getUser.mockResolvedValue({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com'
});

// Partial mocks for complex objects
interface ApiClient {
  baseURL: string;
  timeout: number;
  get<T>(path: string): Promise<T>;
  post<T>(path: string, data: any): Promise<T>;
}

const mockApiClient: Partial<ApiClient> = {
  get: jest.fn(),
  post: jest.fn()
};

// Type-safe spy
const userService = new UserService();
const getUserSpy = jest.spyOn(userService, 'getUser');

getUserSpy.mockResolvedValue({ id: 1, name: 'Bob', email: 'bob@example.com' });

await userService.getUser(1);
expect(getUserSpy).toHaveBeenCalledWith(1);

Example: Module mocking with types

// api.ts
export async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users');
  return response.json();
}

export async function createUser(data: CreateUserData): Promise<User> {
  const response = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(data)
  });
  return response.json();
}

// api.test.ts
import * as api from './api';

jest.mock('./api');

// Type the mocked module
const mockedApi = jest.mocked(api);

describe('User API', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
  
  it('should fetch users', async () => {
    const mockUsers: User[] = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' }
    ];
    
    mockedApi.fetchUsers.mockResolvedValue(mockUsers);
    
    const users = await api.fetchUsers();
    
    expect(users).toHaveLength(2);
    expect(users[0].name).toBe('Alice');
    expect(mockedApi.fetchUsers).toHaveBeenCalledTimes(1);
  });
  
  it('should create user', async () => {
    const newUser: User = { id: 3, name: 'Charlie', email: 'charlie@example.com' };
    const createData: CreateUserData = { name: 'Charlie', email: 'charlie@example.com' };
    
    mockedApi.createUser.mockResolvedValue(newUser);
    
    const result = await api.createUser(createData);
    
    expect(result).toEqual(newUser);
    expect(mockedApi.createUser).toHaveBeenCalledWith(createData);
  });
});

// Using ts-mockito for advanced mocking
import { mock, instance, when, verify, anything } from 'ts-mockito';

class UserRepository {
  async findById(id: number): Promise<User | null> {
    // implementation
  }
}

describe('UserRepository with ts-mockito', () => {
  it('should find user by id', async () => {
    const mockRepo = mock(UserRepository);
    const user: User = { id: 1, name: 'Alice', email: 'alice@example.com' };
    
    when(mockRepo.findById(1)).thenResolve(user);
    
    const repo = instance(mockRepo);
    const result = await repo.findById(1);
    
    expect(result).toEqual(user);
    verify(mockRepo.findById(1)).once();
  });
});

15.4 Test Coverage and Type Coverage Analysis

Tool Command/Config Description Metric
Jest Coverage jest --coverage Measure code coverage - lines, branches, functions Runtime test coverage
type-coverage npx type-coverage Calculate TypeScript type coverage percentage Type annotation coverage
coverageThreshold coverageThreshold: { global: {...} } Enforce minimum coverage requirements CI/CD quality gates
istanbul nyc typescript Code coverage tool used by Jest Coverage reports
strict: true tsconfig.json Enable all strict type-checking options Type safety enforcement
noImplicitAny "noImplicitAny": true Require explicit types, disallow implicit any Explicit typing

Example: Jest coverage configuration

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  collectCoverage: true,
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/index.ts'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  coverageReporters: ['text', 'lcov', 'html', 'json'],
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/__tests__/',
    '/coverage/'
  ]
};

// package.json scripts
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:coverage:watch": "jest --coverage --watchAll",
    "coverage:report": "open coverage/lcov-report/index.html"
  }
}

Example: Type coverage analysis

// Install type-coverage
npm install -D type-coverage

// package.json
{
  "scripts": {
    "type-coverage": "type-coverage",
    "type-coverage:detail": "type-coverage --detail",
    "type-coverage:strict": "type-coverage --strict"
  }
}

// Run type coverage
npx type-coverage

// Output example:
// 2345 / 2456 95.48%
// type-coverage success: 95.48% >= 95.00%

// Detailed report shows files with low coverage
npx type-coverage --detail

// Output:
// path/to/file.ts:45:12 error implicit any
// path/to/file.ts:78:5 error implicit any
// ...

// Configuration in package.json
{
  "typeCoverage": {
    "atLeast": 95,
    "strict": true,
    "ignoreCatch": true,
    "ignoreFiles": [
      "**/*.test.ts",
      "**/*.spec.ts",
      "**/test/**"
    ]
  }
}

// tsconfig.json for strict type checking
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

15.5 Property-Based Testing with TypeScript

Library Feature Description Use Case
fast-check npm i -D fast-check Property-based testing library for TypeScript Generate test cases automatically
fc.property() fc.property(arb, predicate) Define property test with arbitraries Test properties hold for all inputs
Arbitraries fc.integer(), fc.string() Value generators for different types Random test data generation
fc.assert() fc.assert(property) Run property test with generated values Execute property tests
Shrinking Automatic minimal failing case Find smallest input that breaks property Easier debugging
Custom Arbitraries fc.record(), fc.tuple() Create complex type generators Test custom types

Example: Property-based testing with fast-check

import fc from 'fast-check';

// Test that addition is commutative
describe('Addition properties', () => {
  it('should be commutative', () => {
    fc.assert(
      fc.property(fc.integer(), fc.integer(), (a, b) => {
        return a + b === b + a;
      })
    );
  });
  
  it('should be associative', () => {
    fc.assert(
      fc.property(fc.integer(), fc.integer(), fc.integer(), (a, b, c) => {
        return (a + b) + c === a + (b + c);
      })
    );
  });
  
  it('should have identity element (0)', () => {
    fc.assert(
      fc.property(fc.integer(), (a) => {
        return a + 0 === a && 0 + a === a;
      })
    );
  });
});

// Test string reverse function
function reverse(str: string): string {
  return str.split('').reverse().join('');
}

describe('String reverse properties', () => {
  it('reversing twice returns original', () => {
    fc.assert(
      fc.property(fc.string(), (str) => {
        return reverse(reverse(str)) === str;
      })
    );
  });
  
  it('reverse preserves length', () => {
    fc.assert(
      fc.property(fc.string(), (str) => {
        return reverse(str).length === str.length;
      })
    );
  });
});

// Test array sorting
describe('Array sort properties', () => {
  it('sorted array has same length', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = [...arr].sort((a, b) => a - b);
        return sorted.length === arr.length;
      })
    );
  });
  
  it('sorted array is ordered', () => {
    fc.assert(
      fc.property(fc.array(fc.integer()), (arr) => {
        const sorted = [...arr].sort((a, b) => a - b);
        return sorted.every((val, i, arr) =>
          i === 0 || arr[i - 1] <= val
        );
      })
    );
  });
});

Example: Custom arbitraries for complex types

import fc from 'fast-check';

// Define User type
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}

// Custom arbitrary for User
const userArbitrary = fc.record({
  id: fc.integer({ min: 1 }),
  name: fc.string({ minLength: 1, maxLength: 50 }),
  email: fc.emailAddress(),
  age: fc.integer({ min: 18, max: 100 }),
  isActive: fc.boolean()
});

// Test user validation function
function isValidUser(user: User): boolean {
  return (
    user.id > 0 &&
    user.name.length > 0 &&
    user.email.includes('@') &&
    user.age >= 18 &&
    user.age <= 100
  );
}

describe('User validation', () => {
  it('should validate generated users', () => {
    fc.assert(
      fc.property(userArbitrary, (user) => {
        return isValidUser(user);
      })
    );
  });
});

// Complex nested structures
interface Address {
  street: string;
  city: string;
  zipCode: string;
}

interface Company {
  name: string;
  address: Address;
  employees: User[];
}

const addressArbitrary = fc.record({
  street: fc.string(),
  city: fc.string(),
  zipCode: fc.string({ minLength: 5, maxLength: 5 })
});

const companyArbitrary = fc.record({
  name: fc.string(),
  address: addressArbitrary,
  employees: fc.array(userArbitrary, { minLength: 1, maxLength: 100 })
});

// Test company operations
describe('Company operations', () => {
  it('should not lose employees when reorganizing', () => {
    fc.assert(
      fc.property(companyArbitrary, (company) => {
        const originalCount = company.employees.length;
        const reorganized = reorganizeCompany(company);
        return reorganized.employees.length === originalCount;
      })
    );
  });
});

15.6 Contract Testing and API Type Validation

Tool/Library Purpose Description Use Case
Zod npm i zod TypeScript-first schema validation - runtime + types API validation, forms
io-ts npm i io-ts Runtime type checking with static types API boundaries
Yup npm i yup Schema builder with TypeScript support Form validation
Pact @pact-foundation/pact Consumer-driven contract testing Microservices contracts
OpenAPI/Swagger openapi-typescript Generate TypeScript from OpenAPI specs API type generation
MSW npm i -D msw Mock Service Worker with TypeScript API mocking, testing

Example: Zod schema validation

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.number().positive(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().min(18).max(120),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.date(),
  tags: z.array(z.string()).optional()
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Result:
// type User = {
//   id: number;
//   name: string;
//   email: string;
//   age: number;
//   role: "admin" | "user" | "guest";
//   createdAt: Date;
//   tags?: string[] | undefined;
// }

// Validate data
function createUser(data: unknown): User {
  // Throws if validation fails
  return UserSchema.parse(data);
}

// Safe validation
function createUserSafe(data: unknown): { success: true; data: User } | { success: false; error: z.ZodError } {
  const result = UserSchema.safeParse(data);
  return result;
}

// API endpoint validation
app.post('/users', async (req, res) => {
  const result = UserSchema.safeParse(req.body);
  
  if (!result.success) {
    return res.status(400).json({
      error: 'Validation failed',
      details: result.error.issues
    });
  }
  
  const user = result.data; // Typed as User
  await saveUser(user);
  res.status(201).json(user);
});

// Nested schemas
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string().regex(/^\d{5}$/)
});

const CompanySchema = z.object({
  name: z.string(),
  address: AddressSchema,
  employees: z.array(UserSchema),
  revenue: z.number().nonnegative()
});

type Company = z.infer<typeof CompanySchema>;

Example: Contract testing with OpenAPI

// Generate TypeScript types from OpenAPI spec
// npm install -D openapi-typescript
// npx openapi-typescript ./api-spec.yaml -o ./api-types.ts

// api-types.ts (generated)
export interface paths {
  '/users': {
    get: {
      responses: {
        200: {
          content: {
            'application/json': User[];
          };
        };
      };
    };
    post: {
      requestBody: {
        content: {
          'application/json': CreateUserRequest;
        };
      };
      responses: {
        201: {
          content: {
            'application/json': User;
          };
        };
      };
    };
  };
  '/users/{id}': {
    get: {
      parameters: {
        path: {
          id: number;
        };
      };
      responses: {
        200: {
          content: {
            'application/json': User;
          };
        };
      };
    };
  };
}

// Use generated types
import type { paths } from './api-types';

type GetUsersResponse = paths['/users']['get']['responses'][200]['content']['application/json'];
type CreateUserRequest = paths['/users']['post']['requestBody']['content']['application/json'];
type GetUserParams = paths['/users/{id}']['get']['parameters']['path'];

// Type-safe API client
class ApiClient {
  async getUsers(): Promise<GetUsersResponse> {
    const response = await fetch('/users');
    return response.json();
  }
  
  async createUser(data: CreateUserRequest): Promise<User> {
    const response = await fetch('/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    return response.json();
  }
  
  async getUser(params: GetUserParams): Promise<User> {
    const response = await fetch(`/users/${params.id}`);
    return response.json();
  }
}

Example: MSW for API mocking

import { rest } from 'msw';
import { setupServer } from 'msw/node';

// Define mock handlers with types
const server = setupServer(
  rest.get<never, { id: string }, User>('/users/:id', (req, res, ctx) => {
    const { id } = req.params;
    
    return res(
      ctx.status(200),
      ctx.json({
        id: Number(id),
        name: 'John Doe',
        email: 'john@example.com',
        age: 30,
        role: 'user',
        createdAt: new Date()
      })
    );
  }),
  
  rest.post<CreateUserRequest, never, User>('/users', async (req, res, ctx) => {
    const body = await req.json();
    
    return res(
      ctx.status(201),
      ctx.json({
        id: 123,
        ...body,
        createdAt: new Date()
      })
    );
  }),
  
  rest.get<never, never, User[]>('/users', (req, res, ctx) => {
    const page = req.url.searchParams.get('page') || '1';
    
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'Alice', email: 'alice@example.com', age: 25, role: 'user', createdAt: new Date() },
        { id: 2, name: 'Bob', email: 'bob@example.com', age: 30, role: 'admin', createdAt: new Date() }
      ])
    );
  })
);

// Setup for tests
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Use in tests
describe('API integration', () => {
  it('should fetch user by id', async () => {
    const user = await apiClient.getUser({ id: 1 });
    
    expect(user.id).toBe(1);
    expect(user.name).toBe('John Doe');
  });
  
  it('should handle errors', async () => {
    server.use(
      rest.get('/users/:id', (req, res, ctx) => {
        return res(ctx.status(404), ctx.json({ error: 'Not found' }));
      })
    );
    
    await expect(apiClient.getUser({ id: 999 })).rejects.toThrow();
  });
});
Note: Testing best practices:
  • Jest config - Use ts-jest preset, configure coverage thresholds, separate test configs
  • Type-only tests - Use expect-type or @ts-expect-error for compile-time assertions
  • Mocks - Type all mocks with generics, use jest.mocked() for module mocks
  • Coverage - Track both code coverage (Jest) and type coverage (type-coverage tool)
  • Property testing - Use fast-check for automated test case generation
  • Validation - Use Zod or io-ts for runtime validation with TypeScript types

Testing and Quality Assurance Summary

  • Jest - Configure with ts-jest preset, type all mocks and tests for full type safety
  • Type testing - Use expect-type for compile-time type assertions, @ts-expect-error for negative tests
  • Mocking - Type mocks with jest.fn<ReturnType, Args>, use Partial<T> for complex objects
  • Coverage - Measure code coverage with Jest, type coverage with type-coverage, enforce thresholds
  • Property testing - Use fast-check for generative testing with custom arbitraries
  • Validation - Integrate Zod/io-ts for runtime validation, OpenAPI for contract testing

16. Performance Optimization and Build Tools

16.1 Compilation Performance Tuning

Optimization Configuration Impact Trade-off
skipLibCheck "skipLibCheck": true Skip type checking of .d.ts files - 2-5x faster compilation May miss errors in type definitions
incremental "incremental": true Cache compilation info - 40-70% faster rebuilds Requires .tsbuildinfo file storage
skipDefaultLibCheck "skipDefaultLibCheck": true Skip checking default lib.d.ts files Less thorough than skipLibCheck
isolatedModules "isolatedModules": true Ensure each file can be transpiled independently Restricts some TS features (const enums)
Module resolution "moduleResolution": "bundler" Faster resolution for bundlers Only for bundled projects
Exclude patterns "exclude": ["node_modules"] Reduce files to check - significant improvement Must manually include needed files
--diagnostics tsc --diagnostics Show compilation performance metrics Debug information only

Example: Performance-optimized tsconfig.json

// tsconfig.json - Production optimized
{
  "compilerOptions": {
    // Speed optimizations
    "incremental": true,
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    
    // Fast transpilation
    "isolatedModules": true,
    "moduleResolution": "bundler",
    
    // Output settings
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM"],
    
    // Type checking (keep strict for quality)
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    
    // Build caching
    "tsBuildInfoFile": "./dist/.tsbuildinfo",
    
    // Module resolution
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    
    // Output
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": [
    "node_modules",
    "**/*.test.ts",
    "**/*.spec.ts",
    "dist",
    "coverage"
  ]
}

Example: Performance diagnostics

// Run with diagnostics
tsc --diagnostics

// Output example:
Files:              450
Lines:              125000
Nodes:              780000
Identifiers:        245000
Symbols:            198000
Types:              67000
Instantiations:     142000
Memory used:        385MB
I/O read:           0.82s
I/O write:          0.15s
Parse time:         1.45s
Bind time:          0.78s
Check time:         3.21s
Emit time:          0.65s
Total time:         6.09s

// Extended diagnostics for more detail
tsc --diagnostics --extendedDiagnostics

// Analyze slow files
tsc --generateTrace ./trace
// Opens Chrome DevTools Performance tab

Example: Large project optimizations

// For very large codebases (>100k LOC)
{
  "compilerOptions": {
    // Maximum speed settings
    "skipLibCheck": true,
    "incremental": true,
    "isolatedModules": true,
    
    // Reduce type checking scope
    "noEmit": true,  // Let bundler handle transpilation
    
    // Assume changes only affect direct dependencies
    "assumeChangesOnlyAffectDirectDependencies": true,
    
    // Faster but less accurate
    "disableSolutionSearching": true,
    "disableReferencedProjectLoad": true
  },
  // Split into smaller projects
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" },
    { "path": "./packages/utils" }
  ]
}

// Watch mode optimizations
{
  "watchOptions": {
    // Use native file system events (fastest)
    "watchFile": "useFsEvents",
    "watchDirectory": "useFsEvents",
    
    // Fallback options for compatibility
    "fallbackPolling": "dynamicPriority",
    
    // Exclude from watching
    "excludeDirectories": ["**/node_modules", "**/dist"],
    "excludeFiles": ["**/*.test.ts"]
  }
}

16.2 Incremental Builds and Project References

Feature Configuration Benefit Requirement
Incremental Compilation "incremental": true Cache previous compilation results - faster rebuilds .tsbuildinfo file
Composite Projects "composite": true Enable project references - parallel builds declaration: true required
Project References "references": [{ "path": "..." }] Build dependencies separately - faster monorepo builds Composite projects
Build Mode tsc -b or tsc --build Smart dependency tracking - only rebuild changed projects Project references setup
Parallel Builds tsc -b --verbose Build independent projects concurrently Multi-core CPU
Clean Build tsc -b --clean Remove build outputs and caches Fresh build needed

Example: Monorepo with project references

// Root tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/utils" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  },
  "include": ["src/**/*"]
}

// packages/api/tsconfig.json (depends on core and utils)
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "references": [
    { "path": "../core" },
    { "path": "../utils" }
  ]
}

// packages/web/tsconfig.json (depends on api, core, utils)
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx"
  },
  "include": ["src/**/*"],
  "references": [
    { "path": "../api" },
    { "path": "../core" },
    { "path": "../utils" }
  ]
}

Example: Build commands

// package.json scripts
{
  "scripts": {
    // Build all projects with dependencies
    "build": "tsc -b",
    
    // Build specific project
    "build:core": "tsc -b packages/core",
    "build:api": "tsc -b packages/api",
    "build:web": "tsc -b packages/web",
    
    // Watch mode with project references
    "dev": "tsc -b --watch",
    "dev:api": "tsc -b packages/api --watch",
    
    // Clean all build outputs
    "clean": "tsc -b --clean",
    
    // Force rebuild everything
    "rebuild": "tsc -b --force",
    
    // Verbose build output
    "build:verbose": "tsc -b --verbose",
    
    // Dry run (show what would be built)
    "build:dry": "tsc -b --dry"
  }
}

// Build with NPM workspaces (parallel execution)
npm run build --workspaces

// Selective builds based on changes
// Only rebuild changed packages and their dependents
tsc -b packages/core packages/api

// Benefits:
// - Faster builds (only changed projects)
// - Parallel compilation (independent projects)
// - Proper dependency tracking
// - Cached builds (.tsbuildinfo)
// - Better IDE performance

16.3 Bundle Analysis and Tree Shaking with TypeScript

Technique Configuration Purpose Tool
ES Modules "module": "ESNext" Enable tree shaking - remove unused exports All modern bundlers
Side Effects "sideEffects": false in package.json Mark package as side-effect free - aggressive tree shaking Webpack, Rollup
Named Exports export { func } not export default Better tree shaking - individual function removal Best practice
webpack-bundle-analyzer npm i -D webpack-bundle-analyzer Visualize bundle size - identify large dependencies Webpack
source-map-explorer npm i -D source-map-explorer Analyze bundle composition from source maps Any bundler
Const Enums const enum with isolatedModules: false Inline enum values - zero runtime cost TypeScript compiler

Example: Tree-shakeable library structure

// tsconfig.json - for tree-shakeable output
{
  "compilerOptions": {
    "module": "ESNext",           // ES modules for tree shaking
    "target": "ES2020",
    "declaration": true,
    "declarationMap": true,
    "moduleResolution": "bundler",
    
    // Important for tree shaking
    "isolatedModules": true,      // Each file standalone
    "esModuleInterop": true,
    "preserveConstEnums": false   // Inline const enums
  }
}

// package.json
{
  "name": "my-library",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",     // ES module entry
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",  // ES module
      "require": "./dist/index.cjs", // CommonJS
      "types": "./dist/index.d.ts"
    }
  },
  "sideEffects": false  // Enable aggressive tree shaking
}

// src/index.ts - Use named exports
export { funcA } from './moduleA';
export { funcB } from './moduleB';
export { funcC } from './moduleC';

// Avoid default exports for better tree shaking
// ❌ Bad for tree shaking
export default { funcA, funcB, funcC };

// ✅ Good for tree shaking
export { funcA, funcB, funcC };

// Consumer code - only imports what's needed
import { funcA } from 'my-library';  // Only funcA is bundled

Example: Bundle analysis with Webpack

// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  // ... webpack config
  
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
      openAnalyzer: true,
      generateStatsFile: true,
      statsFilename: 'bundle-stats.json'
    })
  ],
  
  optimization: {
    usedExports: true,  // Mark unused exports
    sideEffects: true,  // Respect package.json sideEffects
    concatenateModules: true,  // Scope hoisting
    minimize: true,
    
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
};

// package.json scripts
{
  "scripts": {
    "analyze": "webpack --mode production --profile --json > stats.json && webpack-bundle-analyzer stats.json",
    "build:analyze": "cross-env ANALYZE=true webpack --mode production"
  }
}

// Using source-map-explorer
npm install -D source-map-explorer

// package.json
{
  "scripts": {
    "analyze:sourcemap": "source-map-explorer 'dist/**/*.js'"
  }
}

16.4 Webpack TypeScript Configuration and ts-loader

Option Configuration Purpose Performance
ts-loader npm i -D ts-loader Standard TypeScript loader for Webpack Moderate speed, full type checking
transpileOnly transpileOnly: true Skip type checking during build - much faster 5-10x faster, requires separate type check
fork-ts-checker-webpack-plugin npm i -D fork-ts-checker-webpack-plugin Run type checking in separate process Parallel checking, faster builds
thread-loader npm i -D thread-loader Run loaders in worker pool Parallel processing
cache cache: { type: 'filesystem' } Cache webpack compilation - persistent cache Faster rebuilds
babel-loader + @babel/preset-typescript npm i -D babel-loader @babel/preset-typescript Use Babel for transpilation - no type checking Very fast, type check separately

Example: Webpack with ts-loader (production)

// webpack.config.js
const path = require('path');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/index.ts',
  
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,  // Skip type checking (done by plugin)
              experimentalWatchApi: true,  // Faster watching
              configFile: 'tsconfig.json'
            }
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  
  plugins: [
    // Type checking in separate process
    new ForkTsCheckerWebpackPlugin({
      async: false,  // Wait for type checking in production
      typescript: {
        configFile: path.resolve(__dirname, 'tsconfig.json'),
        diagnosticOptions: {
          semantic: true,
          syntactic: true
        }
      }
    })
  ],
  
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true
  },
  
  // Performance optimizations
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename]
    }
  },
  
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

Example: Fast development config

// webpack.dev.js
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
  mode: 'development',
  devtool: 'eval-source-map',  // Fast source maps
  
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          // Optional: Use thread-loader for parallel processing
          {
            loader: 'thread-loader',
            options: {
              workers: 2,
              workerParallelJobs: 50
            }
          },
          {
            loader: 'ts-loader',
            options: {
              transpileOnly: true,
              happyPackMode: true,  // Required with thread-loader
              experimentalWatchApi: true
            }
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  
  plugins: [
    new ForkTsCheckerWebpackPlugin({
      async: true,  // Don't wait in dev mode (faster)
      typescript: {
        configFile: 'tsconfig.json',
        memoryLimit: 4096  // Increase memory for large projects
      }
    })
  ],
  
  cache: {
    type: 'filesystem',
    allowCollectingMemory: true
  },
  
  devServer: {
    hot: true,
    port: 3000,
    historyApiFallback: true
  }
};

16.5 Vite TypeScript Integration and HMR

Feature Configuration Benefit Note
Native TS Support Zero config - works out of box No loader needed - uses esbuild for transpilation No type checking by default
vite-plugin-checker npm i -D vite-plugin-checker Type checking during dev and build Recommended for type safety
isolatedModules "isolatedModules": true Required for esbuild - each file independent Restricts some TS features
HMR (Hot Module Replacement) Built-in - automatic Instant updates without full reload Preserves component state
Fast Refresh @vitejs/plugin-react React HMR with state preservation Only for React
Build Speed Uses esbuild for dev, Rollup for prod 10-100x faster than Webpack Excellent DX

Example: Vite configuration with TypeScript

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import checker from 'vite-plugin-checker';
import path from 'path';

export default defineConfig({
  plugins: [
    react({
      // Fast Refresh for React
      fastRefresh: true,
      // Babel options if needed
      babel: {
        parserOpts: {
          plugins: ['decorators-legacy']
        }
      }
    }),
    
    // Type checking plugin
    checker({
      typescript: true,
      eslint: {
        lintCommand: 'eslint "./src/**/*.{ts,tsx}"'
      },
      overlay: {
        initialIsOpen: false,  // Don't auto-open error overlay
        position: 'br'  // Bottom right
      }
    })
  ],
  
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@components': path.resolve(__dirname, './src/components'),
      '@utils': path.resolve(__dirname, './src/utils')
    }
  },
  
  build: {
    target: 'es2020',
    outDir: 'dist',
    sourcemap: true,
    
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          utils: ['lodash', 'date-fns']
        }
      }
    },
    
    // Optimize chunk size
    chunkSizeWarningLimit: 1000,
    
    // Minification with esbuild (faster)
    minify: 'esbuild'
  },
  
  server: {
    port: 3000,
    open: true,
    hmr: {
      overlay: true
    }
  },
  
  // Optimize dependencies
  optimizeDeps: {
    include: ['react', 'react-dom'],
    exclude: ['@vite/client', '@vite/env']
  }
});

Example: TypeScript config for Vite

// tsconfig.json for Vite
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    
    // Bundler mode (Vite-specific)
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,  // Required for esbuild
    "noEmit": true,  // Vite handles transpilation
    
    // Type checking
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    
    // Path mapping
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    },
    
    // React
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

// tsconfig.node.json - for Vite config files
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}

// package.json scripts
{
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",  // Type check before build
    "preview": "vite preview",
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch"
  }
}

16.6 esbuild and SWC TypeScript Compilation

Tool Speed Features Limitations
esbuild 10-100x faster than tsc - written in Go Bundling, minification, transpilation, tree shaking No type checking, limited TS features
SWC 20-70x faster than Babel - written in Rust Transpilation, minification, bundling (experimental) No type checking, newer tool
@swc/core npm i -D @swc/core Core SWC library - standalone or with bundlers Node.js API, CLI
esbuild-loader npm i -D esbuild-loader Use esbuild with Webpack - replace ts-loader Much faster Webpack builds
Turbopack Next.js 13+ bundler - Rust-based Incremental bundling, HMR, fast builds Next.js only currently
Type Checking Separate with tsc --noEmit Run type checking in parallel or CI Two-step process

Example: esbuild configuration

// esbuild.config.js
const esbuild = require('esbuild');

// Build configuration
esbuild.build({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outfile: 'dist/bundle.js',
  
  // TypeScript settings
  platform: 'node',
  target: 'es2020',
  format: 'esm',
  
  // Optimizations
  minify: true,
  sourcemap: true,
  treeShaking: true,
  splitting: true,
  
  // External dependencies (not bundled)
  external: ['react', 'react-dom'],
  
  // Loaders for different file types
  loader: {
    '.ts': 'ts',
    '.tsx': 'tsx',
    '.png': 'file',
    '.svg': 'dataurl'
  },
  
  // Define environment variables
  define: {
    'process.env.NODE_ENV': '"production"'
  },
  
  // Watch mode
  watch: false,
  
  // Logging
  logLevel: 'info'
}).catch(() => process.exit(1));

// package.json scripts
{
  "scripts": {
    "build": "npm run type-check && node esbuild.config.js",
    "type-check": "tsc --noEmit",
    "dev": "node esbuild.config.js --watch",
    "dev:check": "concurrently \"npm run type-check:watch\" \"npm run dev\""
  }
}

// Using esbuild programmatically
import * as esbuild from 'esbuild';

// Development with watch
const ctx = await esbuild.context({
  entryPoints: ['src/index.ts'],
  bundle: true,
  outdir: 'dist',
  sourcemap: true,
  platform: 'node',
  target: 'node18'
});

await ctx.watch();
console.log('Watching...');

// Serve mode (dev server)
const { host, port } = await ctx.serve({
  servedir: 'public',
  port: 3000
});
console.log(`Server running at http://${host}:${port}`);

Example: SWC configuration

// .swcrc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true,
      "decorators": true,
      "dynamicImport": true
    },
    "transform": {
      "react": {
        "runtime": "automatic",
        "development": false,
        "refresh": true  // Fast Refresh
      },
      "legacyDecorator": true,
      "decoratorMetadata": true
    },
    "target": "es2020",
    "loose": false,
    "externalHelpers": false,
    "keepClassNames": true
  },
  "module": {
    "type": "es6",
    "strict": false,
    "strictMode": true,
    "lazy": false,
    "noInterop": false
  },
  "minify": false,
  "sourceMaps": true
}

// Using SWC with Node.js
// Install: npm i -D @swc/core @swc/cli
{
  "scripts": {
    "build": "swc src -d dist",
    "build:watch": "swc src -d dist --watch",
    "type-check": "tsc --noEmit"
  }
}

// Using SWC with Webpack
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        use: {
          loader: 'swc-loader',
          options: {
            jsc: {
              parser: {
                syntax: 'typescript',
                tsx: true
              }
            }
          }
        },
        exclude: /node_modules/
      }
    ]
  }
};

// Using esbuild-loader with Webpack
module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'esbuild-loader',
        options: {
          loader: 'tsx',
          target: 'es2020',
          tsconfigRaw: require('./tsconfig.json')
        }
      }
    ]
  },
  plugins: [
    // Use esbuild for minification too (faster)
    new ESBuildMinifyPlugin({
      target: 'es2020',
      css: true
    })
  ]
};

Example: Performance comparison

// Build time comparison for medium project (50k LOC)

// TypeScript Compiler (tsc)
// Time: ~45s
tsc --project tsconfig.json

// Babel + @babel/preset-typescript
// Time: ~35s (no type checking)
babel src --out-dir dist --extensions ".ts,.tsx"

// ts-loader (Webpack)
// Time: ~40s
webpack --mode production

// ts-loader + transpileOnly + fork-ts-checker
// Time: ~25s (parallel type checking)
webpack --mode production

// esbuild-loader (Webpack)
// Time: ~5s (no type checking)
webpack --mode production

// esbuild (standalone)
// Time: ~2s (no type checking)
node esbuild.config.js

// SWC
// Time: ~3s (no type checking)
swc src -d dist

// Vite (esbuild)
// Dev server start: <1s
// Production build: ~5s
vite build

// Recommendation:
// Development: Vite or esbuild with separate tsc --noEmit --watch
// Production: esbuild/SWC + tsc --noEmit in CI pipeline
// Type safety: Always run tsc --noEmit for type checking
Note: Performance optimization best practices:
  • Compilation - Enable skipLibCheck, incremental, use project references for monorepos
  • Build tools - Use esbuild/SWC for fast transpilation, run type checking separately
  • Webpack - Use ts-loader with transpileOnly + fork-ts-checker, enable filesystem cache
  • Vite - Best DX with instant HMR, use vite-plugin-checker for type checking
  • Tree shaking - Use ES modules, named exports, mark packages as side-effect free
  • Analysis - Use webpack-bundle-analyzer or source-map-explorer to identify bloat

Performance and Build Tools Summary

  • Compilation speed - skipLibCheck, incremental builds, project references for 40-70% faster rebuilds
  • Webpack - ts-loader with transpileOnly + fork-ts-checker for parallel type checking
  • Vite - Best development experience with instant HMR, esbuild transpilation, Rollup production builds
  • esbuild - 10-100x faster than tsc, excellent for development and production (no type checking)
  • SWC - Rust-based compiler, 20-70x faster than Babel, growing ecosystem
  • Strategy - Use fast transpiler (esbuild/SWC) + separate type checking with tsc --noEmit

17. Development Tools and IDE Integration

17.1 VS Code TypeScript Features and Extensions

Feature/Extension Purpose Key Functionality Installation
Built-in TypeScript Native TS support - no extension needed IntelliSense, error checking, refactoring, go to definition Included with VS Code
TypeScript Version Choose TS version - workspace or VS Code bundled Switch between versions, test new features Command: "Select TypeScript Version"
ESLint Linting and code quality - dbaeumer.vscode-eslint Real-time linting, auto-fix, rule configuration Extension Marketplace
Prettier Code formatting - esbenp.prettier-vscode Format on save, consistent style, integrates with ESLint Extension Marketplace
Error Lens Inline error messages - usernamehw.errorlens Show errors directly in editor, highlight entire line Extension Marketplace
Pretty TypeScript Errors Better error messages - yoavbls.pretty-ts-errors Format complex errors, explain type mismatches Extension Marketplace
TypeScript Importer Auto-import suggestions - pmneo.tsimporter Suggest imports from workspace, organize imports Extension Marketplace
Path Intellisense File path completion - christian-kohler.path-intellisense Auto-complete file paths in imports Extension Marketplace

Example: VS Code settings for TypeScript

// .vscode/settings.json
{
  // TypeScript settings
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true,
  
  // IntelliSense
  "typescript.suggest.autoImports": true,
  "typescript.suggest.paths": true,
  "typescript.suggest.completeFunctionCalls": true,
  "typescript.preferences.importModuleSpecifier": "relative",
  "typescript.preferences.quoteStyle": "single",
  
  // Error checking
  "typescript.validate.enable": true,
  "typescript.reportStyleChecksAsWarnings": true,
  
  // Inlay hints (TS 4.4+)
  "typescript.inlayHints.parameterNames.enabled": "all",
  "typescript.inlayHints.parameterTypes.enabled": true,
  "typescript.inlayHints.variableTypes.enabled": true,
  "typescript.inlayHints.propertyDeclarationTypes.enabled": true,
  "typescript.inlayHints.functionLikeReturnTypes.enabled": true,
  "typescript.inlayHints.enumMemberValues.enabled": true,
  
  // Organize imports
  "editor.codeActionsOnSave": {
    "source.organizeImports": "explicit",
    "source.fixAll.eslint": "explicit"
  },
  
  // Format on save
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  
  // Quick suggestions
  "editor.quickSuggestions": {
    "strings": true
  },
  "editor.suggest.snippetsPreventQuickSuggestions": false,
  
  // Error display
  "problems.decorations.enabled": true,
  "errorLens.enabled": true,
  "errorLens.delay": 500
}

Example: Useful VS Code keyboard shortcuts

// TypeScript-specific shortcuts in VS Code

// Navigation
F12                 // Go to Definition
Ctrl+F12            // Go to Implementation
Shift+F12           // Find All References
Alt+F12             // Peek Definition
Ctrl+Shift+O        // Go to Symbol in File
Ctrl+T              // Go to Symbol in Workspace

// Refactoring
F2                  // Rename Symbol
Ctrl+.              // Quick Fix / Code Actions
Ctrl+Shift+R        // Refactor menu
Alt+Shift+F         // Format Document

// IntelliSense
Ctrl+Space          // Trigger Suggest
Ctrl+Shift+Space    // Trigger Parameter Hints
Ctrl+I              // Trigger Suggest (alternative)

// Type Information
Ctrl+K Ctrl+I       // Show Hover (type info)

// Imports
Alt+Shift+O         // Organize Imports
Ctrl+Shift+P        // Command Palette
"Add Missing Import"
"Remove Unused Imports"

// Errors
F8                  // Go to Next Error
Shift+F8            // Go to Previous Error
Ctrl+Shift+M        // Show Problems Panel

// TypeScript commands (Ctrl+Shift+P)
"TypeScript: Restart TS Server"
"TypeScript: Select TypeScript Version"
"TypeScript: Go to Source Definition"
"TypeScript: Reload Project"

17.2 ESLint TypeScript Rules and Configuration

Package Purpose Key Rules Installation
@typescript-eslint/parser Parse TypeScript code for ESLint Enables ESLint to understand TS syntax npm i -D @typescript-eslint/parser
@typescript-eslint/eslint-plugin TypeScript-specific ESLint rules 200+ TS-specific rules for best practices npm i -D @typescript-eslint/eslint-plugin
recommended Recommended rule preset Essential rules, minimal configuration Extends plugin:@typescript-eslint/recommended
recommended-type-checked Rules requiring type information More thorough, slower (needs tsconfig) Extends plugin:@typescript-eslint/recommended-type-checked
strict Strictest rule set Enforce best practices, catch more issues Extends plugin:@typescript-eslint/strict
stylistic Code style rules Formatting, naming conventions Extends plugin:@typescript-eslint/stylistic

Example: ESLint configuration for TypeScript

// .eslintrc.js
module.exports = {
  root: true,
  
  // Parser for TypeScript
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module',
    project: './tsconfig.json',  // Required for type-aware rules
    tsconfigRootDir: __dirname
  },
  
  // Plugins
  plugins: ['@typescript-eslint'],
  
  // Extend recommended configs
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-type-checked',
    'plugin:@typescript-eslint/stylistic'
  ],
  
  // Custom rules
  rules: {
    // TypeScript-specific
    '@typescript-eslint/explicit-function-return-type': 'warn',
    '@typescript-eslint/explicit-module-boundary-types': 'warn',
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/no-unused-vars': ['error', {
      argsIgnorePattern: '^_',
      varsIgnorePattern: '^_'
    }],
    '@typescript-eslint/no-non-null-assertion': 'warn',
    '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
    '@typescript-eslint/consistent-type-imports': ['error', {
      prefer: 'type-imports',
      fixStyle: 'inline-type-imports'
    }],
    
    // Naming conventions
    '@typescript-eslint/naming-convention': [
      'error',
      {
        selector: 'interface',
        format: ['PascalCase'],
        custom: {
          regex: '^I[A-Z]',
          match: false
        }
      },
      {
        selector: 'typeAlias',
        format: ['PascalCase']
      },
      {
        selector: 'enum',
        format: ['PascalCase']
      },
      {
        selector: 'enumMember',
        format: ['UPPER_CASE']
      }
    ],
    
    // Type-aware rules (require parserOptions.project)
    '@typescript-eslint/await-thenable': 'error',
    '@typescript-eslint/no-floating-promises': 'error',
    '@typescript-eslint/no-misused-promises': 'error',
    '@typescript-eslint/require-await': 'error',
    '@typescript-eslint/no-unnecessary-type-assertion': 'error',
    '@typescript-eslint/prefer-nullish-coalescing': 'warn',
    '@typescript-eslint/prefer-optional-chain': 'warn',
    '@typescript-eslint/strict-boolean-expressions': 'warn',
    
    // Disable base ESLint rules (replaced by TS versions)
    'no-unused-vars': 'off',
    'no-undef': 'off',  // TypeScript handles this
    'no-use-before-define': 'off'
  },
  
  // Override for specific files
  overrides: [
    {
      files: ['*.test.ts', '*.spec.ts'],
      rules: {
        '@typescript-eslint/no-explicit-any': 'off'
      }
    }
  ]
};
// Key rules to enforce code quality

// Prevent common mistakes
'@typescript-eslint/no-explicit-any': 'error'
'@typescript-eslint/no-unsafe-assignment': 'error'
'@typescript-eslint/no-unsafe-call': 'error'
'@typescript-eslint/no-unsafe-member-access': 'error'
'@typescript-eslint/no-unsafe-return': 'error'

// Promise handling
'@typescript-eslint/no-floating-promises': 'error'
'@typescript-eslint/no-misused-promises': 'error'
'@typescript-eslint/await-thenable': 'error'
'@typescript-eslint/promise-function-async': 'warn'

// Type safety
'@typescript-eslint/no-unnecessary-type-assertion': 'error'
'@typescript-eslint/no-non-null-assertion': 'warn'
'@typescript-eslint/prefer-as-const': 'error'
'@typescript-eslint/switch-exhaustiveness-check': 'error'

// Modern TypeScript features
'@typescript-eslint/prefer-nullish-coalescing': 'warn'
'@typescript-eslint/prefer-optional-chain': 'warn'
'@typescript-eslint/prefer-readonly': 'warn'
'@typescript-eslint/prefer-reduce-type-parameter': 'warn'

// Code style
'@typescript-eslint/array-type': ['error', { default: 'array-simple' }]
'@typescript-eslint/consistent-type-definitions': ['error', 'interface']
'@typescript-eslint/consistent-type-imports': 'error'
'@typescript-eslint/method-signature-style': ['error', 'property']

// package.json scripts
{
  "scripts": {
    "lint": "eslint . --ext .ts,.tsx",
    "lint:fix": "eslint . --ext .ts,.tsx --fix",
    "lint:report": "eslint . --ext .ts,.tsx --format html --output-file eslint-report.html"
  }
}

17.3 Prettier TypeScript Formatting

Configuration Purpose Value Note
parser Auto-detected for .ts/.tsx files "typescript" Built-in Prettier support
printWidth Line length before wrapping 80 or 100 Team preference
tabWidth Spaces per indentation level 2 or 4 Consistent with team
semi Add semicolons true TypeScript convention
singleQuote Use single quotes true Common preference
trailingComma Trailing commas in arrays/objects "all" or "es5" "all" recommended for TS
arrowParens Arrow function parentheses "always" Consistent with types

Example: Prettier configuration

// .prettierrc.json
{
  "semi": true,
  "trailingComma": "all",
  "singleQuote": true,
  "printWidth": 100,
  "tabWidth": 2,
  "arrowParens": "always",
  "endOfLine": "lf",
  "bracketSpacing": true,
  "jsxSingleQuote": false,
  "quoteProps": "as-needed"
}

// .prettierignore
node_modules
dist
build
coverage
*.min.js
*.min.css
.next
.cache

// package.json scripts
{
  "scripts": {
    "format": "prettier --write \"src/**/*.{ts,tsx}\"",
    "format:check": "prettier --check \"src/**/*.{ts,tsx}\""
  }
}

// Integrate with ESLint
// npm install -D eslint-config-prettier eslint-plugin-prettier

// .eslintrc.js
module.exports = {
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'  // Disables ESLint formatting rules that conflict with Prettier
  ],
  plugins: ['prettier'],
  rules: {
    'prettier/prettier': 'error'  // Show Prettier errors as ESLint errors
  }
};

Example: Format on save and pre-commit

// VS Code settings.json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "[typescript]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "explicit"
  }
}

// Pre-commit hooks with Husky + lint-staged
// npm install -D husky lint-staged

// package.json
{
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ]
  }
}

// .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged

// Initialize husky
npm pkg set scripts.prepare="husky install"
npm run prepare
npx husky add .husky/pre-commit "npx lint-staged"

17.4 TypeScript Language Server and LSP

Concept Description Functionality Performance
Language Server Protocol Standard protocol for editor-language communication IntelliSense, diagnostics, refactoring across editors Enables rich editing features
tsserver TypeScript language server - included with TypeScript Powers all IDE features - runs in background Can be memory-intensive
Project Loading Load tsconfig.json and all referenced files Type checking, IntelliSense, navigation Initial load can be slow
Incremental Updates Only recheck changed files and dependencies Fast feedback after initial load Efficient for large projects
Memory Management Cache type information, clean up unused data Keep IDE responsive with large codebases Configurable limits
Restart Server Clear cache, reload configuration Fix stuck state, apply config changes Command: "TypeScript: Restart TS Server"

Example: Language server configuration

// VS Code settings for TypeScript language server
{
  // Language server settings
  "typescript.tsserver.maxTsServerMemory": 8192,  // MB (default: 3072)
  "typescript.tsserver.log": "off",  // or "verbose" for debugging
  "typescript.tsserver.trace": "off",
  
  // Performance optimizations
  "typescript.disableAutomaticTypeAcquisition": false,
  "typescript.tsserver.watchOptions": {
    "excludeDirectories": [
      "**/node_modules",
      "**/.git",
      "**/dist",
      "**/build"
    ]
  },
  
  // Project loading
  "typescript.tsserver.experimental.enableProjectDiagnostics": true,
  "typescript.tsserver.useSyntaxServer": "auto",  // Separate server for syntax
  
  // Plugin support
  "typescript.tsserver.pluginPaths": [],
  
  // Surveys and telemetry
  "typescript.surveys.enabled": false
}

// tsconfig.json optimizations for language server
{
  "compilerOptions": {
    "skipLibCheck": true,  // Faster project loading
    "incremental": true,   // Cache between restarts
    "tsBuildInfoFile": "./.cache/.tsbuildinfo"
  },
  "exclude": [
    "node_modules",
    "dist",
    "**/*.test.ts",
    "**/*.spec.ts"
  ]
}

// Restart language server when needed
// Ctrl+Shift+P → "TypeScript: Restart TS Server"

// View language server logs
// Ctrl+Shift+P → "TypeScript: Open TS Server log"

// Select TypeScript version (workspace vs VS Code)
// Ctrl+Shift+P → "TypeScript: Select TypeScript Version"

Example: Language server plugins

// TypeScript language service plugins extend IDE features

// Example: typescript-plugin-styled-components
// Provides IntelliSense for styled-components

// Install plugin
npm install -D typescript-plugin-styled-components

// tsconfig.json
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-plugin-styled-components",
        "minify": true,
        "transpileTemplateLiterals": true,
        "ssr": false
      }
    ]
  }
}

// Example: typescript-plugin-css-modules
// Type safety for CSS modules

npm install -D typescript-plugin-css-modules

// tsconfig.json
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "typescript-plugin-css-modules",
        "options": {
          "classnameTransform": "camelCase",
          "customTemplate": "./cssModuleTemplate.js"
        }
      }
    ]
  }
}

// VS Code settings to use workspace TypeScript (required for plugins)
{
  "typescript.tsdk": "node_modules/typescript/lib",
  "typescript.enablePromptUseWorkspaceTsdk": true
}

// Other useful plugins:
// - @styled/typescript-styled-plugin (Styled Components)
// - typescript-lit-html-plugin (lit-html templates)
// - typescript-svelte-plugin (Svelte files)
// - @volar/typescript-plugin (Vue)

17.5 Refactoring Tools and Code Actions

Refactoring Action Shortcut Use Case
Rename Symbol Rename across all files F2 Update variable, function, class names everywhere
Extract Function Pull code into new function Ctrl+. → "Extract to function" Reduce complexity, improve reusability
Extract Constant Create named constant from value Ctrl+. → "Extract to constant" Magic numbers, repeated values
Extract Type Create type alias from inline type Ctrl+. → "Extract to type alias" Reuse complex types
Move to File Move symbol to new/existing file Ctrl+. → "Move to file" Organize code, split large files
Convert Import Switch between import styles Ctrl+. → "Convert to ES module" Modernize CommonJS imports
Infer Function Return Type Add explicit return type annotation Ctrl+. → "Infer return type" Better documentation, type safety
Add Missing Member Implement interface/abstract methods Ctrl+. → "Add all missing members" Implement contracts quickly

Example: Common code actions

// Quick Fix (Ctrl+. or lightbulb icon) - Common actions

// 1. Add missing imports
import { useState } from 'react';  // Auto-suggested

// 2. Implement interface
class User implements IUser {  // Ctrl+. → "Implement interface"
  // All interface methods added automatically
}

// 3. Add inferred type
function calculate(a: number, b: number) {  // Ctrl+. → "Infer return type"
  return a + b;
}
// Becomes:
function calculate(a: number, b: number): number {
  return a + b;
}

// 4. Convert to arrow function
function greet(name: string) {  // Ctrl+. → "Convert to arrow function"
  return `Hello, ${name}`;
}
// Becomes:
const greet = (name: string) => `Hello, ${name}`;

// 5. Extract to function
const result = users
  .filter(u => u.age > 18)  // Select code → Ctrl+. → "Extract to function"
  .map(u => u.name);
// Becomes:
const getAdultNames = (users: User[]) => 
  users.filter(u => u.age > 18).map(u => u.name);
const result = getAdultNames(users);

// 6. Convert to async/await
fetchData()  // Ctrl+. → "Convert to async/await"
  .then(data => process(data))
  .catch(err => console.error(err));
// Becomes:
try {
  const data = await fetchData();
  process(data);
} catch (err) {
  console.error(err);
}

// 7. Add JSDoc comment
function complexFunction(params) {  // Ctrl+. → "Add JSDoc comment"
  // ...
}
// Becomes:
/**
 * 
 * @param params 
 * @returns 
 */
function complexFunction(params) {
  // ...
}

Example: Organize imports

// Before - messy imports
import { z } from 'zod';
import React from 'react';
import { useState } from 'react';
import './styles.css';
import { formatDate } from '../utils';
import type { User } from './types';
import { api } from '@/api';

// After - organized (Alt+Shift+O or save with organizeImports)
import './styles.css';

import React, { useState } from 'react';
import { z } from 'zod';

import { api } from '@/api';
import { formatDate } from '../utils';
import type { User } from './types';

// VS Code settings for import organization
{
  "editor.codeActionsOnSave": {
    "source.organizeImports": "explicit"
  },
  "typescript.preferences.importModuleSpecifier": "relative",
  "typescript.preferences.importModuleSpecifierEnding": "auto"
}

// Remove unused imports automatically
import { useState, useEffect, useMemo } from 'react';  // useMemo unused
// After save (if configured):
import { useState, useEffect } from 'react';

// ESLint rule to enforce
{
  "rules": {
    "@typescript-eslint/no-unused-vars": ["error", {
      "vars": "all",
      "args": "after-used",
      "ignoreRestSiblings": false
    }]
  }
}

17.6 IntelliSense and Auto-completion Features

Feature Description Trigger Benefit
Auto-completion Suggest completions based on context Ctrl+Space or automatic Faster coding, discover APIs
Parameter Hints Show function signature while typing Ctrl+Shift+Space Know parameters without docs
Type Information Show type on hover Hover or Ctrl+K Ctrl+I Understand types quickly
Quick Info Show JSDoc, signature, type details Hover over symbol Documentation at cursor
Import Suggestions Auto-suggest imports for unresolved symbols Automatic on type No manual import hunting
Path Completion Auto-complete file paths in imports Type in import string Avoid typos, find files fast
Snippet Completion Insert code templates Type snippet prefix Boilerplate code quickly
Inlay Hints Show inline type annotations Automatic display Understand inferred types

Example: IntelliSense configuration

// VS Code settings for enhanced IntelliSense
{
  // Auto-completion
  "editor.quickSuggestions": {
    "other": true,
    "comments": false,
    "strings": true  // Show suggestions in strings
  },
  "editor.suggestOnTriggerCharacters": true,
  "editor.acceptSuggestionOnCommitCharacter": true,
  "editor.acceptSuggestionOnEnter": "on",
  "editor.tabCompletion": "on",
  "editor.wordBasedSuggestions": "matchingDocuments",
  
  // TypeScript IntelliSense
  "typescript.suggest.autoImports": true,
  "typescript.suggest.paths": true,
  "typescript.suggest.completeFunctionCalls": true,
  "typescript.suggest.includeCompletionsForImportStatements": true,
  "typescript.suggest.includeCompletionsWithSnippetText": true,
  
  // Parameter hints
  "editor.parameterHints.enabled": true,
  "editor.parameterHints.cycle": true,
  
  // Inlay hints (TypeScript 4.4+)
  "typescript.inlayHints.parameterNames.enabled": "all",
  "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true,
  "typescript.inlayHints.parameterTypes.enabled": true,
  "typescript.inlayHints.variableTypes.enabled": true,
  "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": true,
  "typescript.inlayHints.propertyDeclarationTypes.enabled": true,
  "typescript.inlayHints.functionLikeReturnTypes.enabled": true,
  "typescript.inlayHints.enumMemberValues.enabled": true,
  
  // Quick info
  "editor.hover.enabled": true,
  "editor.hover.delay": 300,
  "editor.hover.sticky": true
}

Example: IntelliSense in action

// 1. Auto-import suggestions
const data = us|  // Type 'us' → suggests 'useState' with auto-import
// Press Enter → import { useState } from 'react';

// 2. Parameter hints
Array.from(|)  // Ctrl+Shift+Space shows:
// from<T>(arrayLike: ArrayLike<T>): T[]
// from<T, U>(arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => U, thisArg?: any): U[]

// 3. Type information on hover
const user = { name: 'Alice', age: 30 };
// Hover shows: const user: { name: string; age: number; }

// 4. Inlay hints display
function greet(name) {  // Shows: name: string
  return `Hello, ${name}`;
}
const result = greet('Alice');  // Shows: const result: string

// 5. Dot completion for object properties
interface User {
  id: number;
  name: string;
  email: string;
}
const user: User = {|  // Ctrl+Space shows: id, name, email suggestions

// 6. Generic type inference
const array = [1, 2, 3];
array.map(|  // Shows: (value: number, index: number, array: number[]) => U

// 7. Union type narrowing
function handle(value: string | number) {
  if (typeof value === 'string') {
    value.|  // IntelliSense shows string methods
  } else {
    value.|  // IntelliSense shows number methods
  }
}

// 8. JSDoc integration
/**
 * Formats a user's name
 * @param user - The user object
 * @param includeTitle - Whether to include title
 * @returns Formatted name string
 */
function formatName(user: User, includeTitle = false): string {
  // Hover shows full JSDoc
}

// 9. Path completion in imports
import { Button } from './components/|  // Shows directory contents

// 10. Template literal type completion
type Color = 'red' | 'green' | 'blue';
const color: Color = '|  // Shows: red, green, blue
Note: Development tools best practices:
  • VS Code - Use workspace TypeScript version, enable inlay hints, install recommended extensions
  • ESLint - Use @typescript-eslint with type-aware rules, integrate with Prettier
  • Prettier - Configure for consistency, set up pre-commit hooks with Husky + lint-staged
  • Language Server - Increase memory for large projects, use plugins for framework support
  • Refactoring - Use built-in refactorings (F2, Ctrl+.), organize imports automatically
  • IntelliSense - Enable all suggestions, parameter hints, inlay hints for maximum productivity

Development Tools and IDE Integration Summary

  • VS Code - Best-in-class TypeScript support with IntelliSense, refactoring, debugging, extensions
  • ESLint - Use @typescript-eslint/parser and plugin with recommended-type-checked rules
  • Prettier - Automatic formatting with ESLint integration, format on save and pre-commit
  • Language Server - Powers all IDE features, configure memory limits, use plugins for frameworks
  • Refactoring - Extract functions/types, rename symbols, organize imports, convert code patterns
  • IntelliSense - Auto-completion, parameter hints, type information, inlay hints for productive coding

18. Error Handling and Debugging Techniques

18.1 TypeScript Compiler Error Messages

Error Code Message Pattern Common Cause Solution
TS2322 Type 'X' is not assignable to type 'Y' Type mismatch in assignment Check types, add type assertion, or fix source type
TS2345 Argument of type 'X' is not assignable to parameter of type 'Y' Wrong function argument type Pass correct type or update function signature
TS2339 Property 'X' does not exist on type 'Y' Accessing non-existent property Add property to type, use optional chaining, or type guard
TS2571 Object is of type 'unknown' Using unknown type without narrowing Add type guard or type assertion
TS2769 No overload matches this call Function call doesn't match any signature Check argument types and count
TS2740 Type 'X' is missing properties from type 'Y' Incomplete object initialization Add missing properties or use Partial<T>
TS7006 Parameter 'X' implicitly has an 'any' type Missing type annotation with noImplicitAny Add explicit type annotation
TS2304 Cannot find name 'X' Undefined variable/type, missing import Import symbol or install @types package

Example: Common compiler errors and fixes

// TS2322 - Type mismatch
// ❌ Error
const num: number = '42';  // Type 'string' is not assignable to type 'number'
// ✅ Fix
const num: number = 42;

// TS2345 - Wrong argument type
// ❌ Error
function greet(name: string) { }
greet(42);  // Argument of type 'number' is not assignable to parameter of type 'string'
// ✅ Fix
greet('Alice');

// TS2339 - Property doesn't exist
// ❌ Error
interface User { name: string; }
const user: User = { name: 'Alice' };
console.log(user.age);  // Property 'age' does not exist on type 'User'
// ✅ Fix 1 - Add to interface
interface User { name: string; age?: number; }
// ✅ Fix 2 - Type guard
if ('age' in user) {
  console.log(user.age);
}

// TS2571 - Unknown type
// ❌ Error
function process(data: unknown) {
  console.log(data.name);  // Object is of type 'unknown'
}
// ✅ Fix - Type guard
function process(data: unknown) {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    console.log((data as { name: string }).name);
  }
}

// TS2769 - No matching overload
// ❌ Error
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) { return a + b; }
add(1, '2');  // No overload matches this call
// ✅ Fix - Use compatible types
add(1, 2);  // OK
add('1', '2');  // OK

// TS2740 - Missing properties
// ❌ Error
interface Config {
  host: string;
  port: number;
  secure: boolean;
}
const config: Config = { host: 'localhost' };  // Missing 'port' and 'secure'
// ✅ Fix - Provide all properties
const config: Config = { host: 'localhost', port: 3000, secure: true };

// TS7006 - Implicit any
// ❌ Error (with noImplicitAny)
function process(data) {  // Parameter 'data' implicitly has an 'any' type
  console.log(data);
}
// ✅ Fix - Add type annotation
function process(data: unknown) {
  console.log(data);
}

Example: Understanding error messages

// Complex error message breakdown

// Error:
// Type '{ name: string; age: number; }' is not assignable to type 'User'.
//   Property 'email' is missing in type '{ name: string; age: number; }' 
//   but required in type 'User'.

interface User {
  name: string;
  age: number;
  email: string;  // Required property
}

const user: User = {  // ❌ Error
  name: 'Alice',
  age: 30
  // Missing: email
};

// Reading the error:
// 1. Main message: type mismatch
// 2. Specific issue: missing 'email' property
// 3. Location: 'email' is required in User interface

// Error with complex type
type Handler<T> = (data: T) => void;

const handler: Handler<string> = (data: number) => {  // ❌ Error
  console.log(data);
};

// Error: Type '(data: number) => void' is not assignable to type 'Handler<string>'.
//   Types of parameters 'data' and 'data' are incompatible.
//     Type 'string' is not assignable to type 'number'.

// Understanding:
// 1. Handler expects string parameter
// 2. Function defined with number parameter
// 3. Parameter types are incompatible

// Use --explainFiles flag for more details
tsc --explainFiles

// Use --noErrorTruncation for full error messages
tsc --noErrorTruncation

18.2 Type Error Diagnosis and Resolution

Technique Method When to Use Benefit
Hover for Type Info Hover over symbol in IDE Understand inferred types, signatures See what TypeScript thinks the type is
Go to Definition F12 on symbol Find type definitions, interfaces Understand structure and constraints
Type Annotations Add explicit types to narrow inference Disambiguate complex types Control type inference, catch errors early
@ts-expect-error Suppress error, mark as intentional Known type issue, waiting for fix Document known limitations
@ts-ignore Suppress next line error (avoid if possible) Last resort for unfixable issues Bypass type checker (dangerous)
Type Assertion Use as Type to override inference You know more than compiler Tell compiler the actual type
Isolate Problem Extract to separate file/function Complex type errors Reduce complexity, test in isolation
--noEmit Type check without generating output Fast error checking Quick validation in CI/CD

Example: Diagnosis techniques

// 1. Use type annotations to narrow inference
// Problem: Generic type too broad
const data = [];  // Inferred as: never[]
data.push(1);  // ❌ Error

// Solution: Add type annotation
const data: number[] = [];
data.push(1);  // ✅ OK

// 2. Break down complex types
// Problem: Complex union causing issues
type ComplexType = 
  | { type: 'a'; value: string }
  | { type: 'b'; value: number }
  | { type: 'c'; value: boolean };

function handle(data: ComplexType) {
  // Hard to debug all at once
}

// Solution: Test each branch separately
function handleA(data: Extract<ComplexType, { type: 'a' }>) {
  console.log(data.value.toUpperCase());  // string methods available
}

// 3. Use @ts-expect-error for known issues
// Third-party library has incorrect types
// @ts-expect-error - Library types are wrong, will be fixed in v2.0
const result = someLibraryFunction('param');

// 4. Type assertions when you know better than compiler
const canvas = document.getElementById('canvas');  // HTMLElement | null
// You know it's a canvas element
const ctx = (canvas as HTMLCanvasElement).getContext('2d');

// 5. Diagnostic types to inspect complex types
type Inspect<T> = { [K in keyof T]: T[K] };

type Complex = Partial<Omit<User, 'id'>> & { active: boolean };
type Simplified = Inspect<Complex>;  // Hover to see resolved type

// 6. Check assignability with conditional types
type IsAssignable<T, U> = T extends U ? true : false;

type Test1 = IsAssignable<string, string | number>;  // true
type Test2 = IsAssignable<number, string>;  // false

// 7. Use --traceResolution to debug module resolution
// tsc --traceResolution | grep "my-module"

// 8. Enable verbose errors in tsconfig
{
  "compilerOptions": {
    "noErrorTruncation": true  // Show full error messages
  }
}

Example: Debugging type inference

// Helper type to see inferred types
type Debug<T> = { [K in keyof T]: T[K] } & {};

// Example: Understanding generic inference
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
  return array.map(fn);
}

// Hover over each to see inferred types
const numbers = [1, 2, 3];
const strings = map(numbers, n => n.toString());
// T = number, U = string, result = string[]

// Use satisfies to validate without changing type
const config = {
  host: 'localhost',
  port: 3000
} satisfies Record<string, string | number>;

config.host;  // Still inferred as 'localhost' literal type

// Debugging discriminated unions
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

function unwrap<T>(result: Result<T>): T {
  if (result.success) {
    // Hover over result.data to see narrowing
    return result.data;  // TypeScript knows: { success: true; data: T }
  }
  throw new Error(result.error);  // TypeScript knows: { success: false; error: string }
}

// Debug utility to force type errors (see full type)
type ForceError<T> = T extends never ? T : never;

// Uncomment to see full type structure
// type DebugMyType = ForceError<ComplexType>;

// Check if types match
type Equals<X, Y> = 
  (<T>() => T extends X ? 1 : 2) extends 
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type Test = Equals<{ a: number }, { a: number }>;  // true

18.3 Source Map Configuration and Debugging

Option Configuration Purpose Use Case
sourceMap "sourceMap": true Generate .js.map files - map compiled to source Debug TypeScript in browser/Node.js
inlineSourceMap "inlineSourceMap": true Embed source maps in .js files Single-file distribution
inlineSources "inlineSources": true Include TypeScript source in map No separate .ts files needed for debugging
declarationMap "declarationMap": true Generate .d.ts.map for declaration files Go to source from .d.ts files
sourceRoot "sourceRoot": "./src" Specify root for source files in maps Correct source paths in debugger
mapRoot "mapRoot": "./maps" Specify location of map files Separate map file deployment
VS Code Debugging launch.json configuration Debug TypeScript directly in VS Code Breakpoints in .ts files

Example: Source map configuration

// tsconfig.json for debugging
{
  "compilerOptions": {
    // Source map generation
    "sourceMap": true,           // Generate .js.map files
    "declarationMap": true,      // Generate .d.ts.map files
    
    // Inline options (for bundlers)
    "inlineSourceMap": false,    // Don't inline (separate files)
    "inlineSources": false,      // Don't embed TypeScript source
    
    // Source paths
    "sourceRoot": "",            // Relative to source map
    "mapRoot": "",               // Where to find maps
    
    // Output settings
    "outDir": "./dist",
    "rootDir": "./src",
    
    // Keep for better debugging
    "removeComments": false,
    "preserveConstEnums": true
  }
}

// For production (smaller bundles)
{
  "compilerOptions": {
    "sourceMap": false,          // No source maps
    "removeComments": true,      // Strip comments
    "declaration": false         // No .d.ts files
  }
}

// For development (best debugging)
{
  "compilerOptions": {
    "sourceMap": true,
    "declarationMap": true,
    "inlineSources": true,       // Embed sources (easier debugging)
    "removeComments": false
  }
}

// Webpack source map options
module.exports = {
  mode: 'development',
  devtool: 'inline-source-map',  // Best for development
  // devtool: 'source-map',      // Separate files (production)
  // devtool: 'eval-source-map', // Fast rebuild (development)
  // devtool: 'cheap-source-map', // Faster, less accurate
};

Example: VS Code debugging configuration

// .vscode/launch.json - Node.js debugging
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug TypeScript",
      "program": "${workspaceFolder}/src/index.ts",
      "preLaunchTask": "tsc: build - tsconfig.json",
      "outFiles": ["${workspaceFolder}/dist/**/*.js"],
      "sourceMaps": true,
      "skipFiles": ["<node_internals>/**"],
      "console": "integratedTerminal"
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug with ts-node",
      "runtimeArgs": ["-r", "ts-node/register"],
      "args": ["${workspaceFolder}/src/index.ts"],
      "skipFiles": ["<node_internals>/**"],
      "console": "integratedTerminal"
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Jest Tests",
      "program": "${workspaceFolder}/node_modules/.bin/jest",
      "args": ["--runInBand", "--no-cache"],
      "console": "integratedTerminal",
      "internalConsoleOptions": "neverOpen"
    }
  ]
}

// .vscode/launch.json - Chrome debugging (React, etc.)
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}/src",
      "sourceMapPathOverrides": {
        "webpack:///src/*": "${webRoot}/*"
      }
    }
  ]
}

// .vscode/tasks.json - Build before debugging
{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "typescript",
      "tsconfig": "tsconfig.json",
      "option": "watch",
      "problemMatcher": ["$tsc-watch"],
      "group": "build",
      "label": "tsc: watch - tsconfig.json"
    }
  ]
}

18.4 Runtime Error Handling with Type Safety

Pattern Implementation Type Safety Use Case
Result Type Discriminated union for success/failure Force error handling at compile time Avoid throwing exceptions, functional approach
Try-Catch with Typing Type the error with type guards Handle specific error types safely API calls, file operations
Custom Error Classes Extend Error with additional properties Discriminate error types Domain-specific errors
Error Boundary Pattern Wrap risky operations with error handling Isolate errors, prevent propagation React error boundaries, middleware
Validation Libraries Zod, io-ts, Yup for runtime validation Runtime and compile-time safety API responses, user input
Never Type Functions that never return (throw/exit) Document unreachable code Error handlers, infinite loops
Option/Maybe Type Represent nullable values explicitly Force null checking Functional programming patterns

Example: Result type pattern

// Result type for error handling without exceptions
type Result<T, E = Error> = 
  | { success: true; value: T }
  | { success: false; error: E };

// Usage
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { success: false, error: 'Division by zero' };
  }
  return { success: true, value: a / b };
}

// Consumer must handle both cases
const result = divide(10, 2);
if (result.success) {
  console.log(result.value);  // TypeScript knows: value exists
} else {
  console.error(result.error);  // TypeScript knows: error exists
}

// Helper functions
function ok<T>(value: T): Result<T, never> {
  return { success: true, value };
}

function err<E>(error: E): Result<never, E> {
  return { success: false, error };
}

// Async version
async function fetchUser(id: string): Promise<Result<User, string>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return err(`Failed to fetch user: ${response.status}`);
    }
    const user = await response.json();
    return ok(user);
  } catch (error) {
    return err(`Network error: ${error}`);
  }
}

// Chain results
function map<T, U, E>(
  result: Result<T, E>,
  fn: (value: T) => U
): Result<U, E> {
  return result.success ? ok(fn(result.value)) : result;
}

const doubled = map(divide(10, 2), n => n * 2);

Example: Custom error classes and type guards

// Custom error hierarchy
class AppError extends Error {
  constructor(message: string) {
    super(message);
    this.name = this.constructor.name;
    Object.setPrototypeOf(this, new.target.prototype);
  }
}

class ValidationError extends AppError {
  constructor(
    message: string,
    public field: string,
    public value: unknown
  ) {
    super(message);
  }
}

class NetworkError extends AppError {
  constructor(
    message: string,
    public statusCode: number,
    public url: string
  ) {
    super(message);
  }
}

class NotFoundError extends AppError {
  constructor(
    public resource: string,
    public id: string
  ) {
    super(`${resource} with id ${id} not found`);
  }
}

// Type guards for error handling
function isValidationError(error: unknown): error is ValidationError {
  return error instanceof ValidationError;
}

function isNetworkError(error: unknown): error is NetworkError {
  return error instanceof NetworkError;
}

function isNotFoundError(error: unknown): error is NotFoundError {
  return error instanceof NotFoundError;
}

// Usage with proper typing
async function getUserData(id: string): Promise<User> {
  try {
    const response = await fetch(`/api/users/${id}`);
    
    if (!response.ok) {
      if (response.status === 404) {
        throw new NotFoundError('User', id);
      }
      throw new NetworkError(
        'Failed to fetch user',
        response.status,
        response.url
      );
    }
    
    const data = await response.json();
    
    if (!data.email) {
      throw new ValidationError(
        'Email is required',
        'email',
        data.email
      );
    }
    
    return data;
    
  } catch (error) {
    // Type-safe error handling
    if (isValidationError(error)) {
      console.error(`Validation failed for ${error.field}:`, error.value);
    } else if (isNetworkError(error)) {
      console.error(`Network error ${error.statusCode} at ${error.url}`);
    } else if (isNotFoundError(error)) {
      console.error(`${error.resource} not found: ${error.id}`);
    } else if (error instanceof Error) {
      console.error('Unexpected error:', error.message);
    } else {
      console.error('Unknown error:', error);
    }
    throw error;
  }
}

// Generic error handler
function handleError(error: unknown): never {
  if (error instanceof AppError) {
    console.error(`[${error.name}] ${error.message}`);
  } else if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error('Unknown error:', error);
  }
  process.exit(1);
}

18.5 Type-only Imports for Debugging

Feature Syntax Purpose Benefit
Type-only Import import type { T } from './module' Import only for type checking - removed at runtime No runtime overhead, clear intent
Inline Type Import import { type T, value } from './module' Mix type and value imports Cleaner syntax, single import statement
Type-only Export export type { T } from './module' Re-export only types Clear API boundaries
importsNotUsedAsValues "verbatimModuleSyntax": true Preserve or remove unused imports Smaller bundles, clearer semantics
isolatedModules "isolatedModules": true Ensure each file can transpile independently Required for esbuild, SWC
preserveValueImports "preserveValueImports": true Keep imports even if only for types Side-effect preservation

Example: Type-only imports

// types.ts
export interface User {
  id: string;
  name: string;
}

export class UserService {
  getUser(id: string): User {
    // ...
  }
}

export const API_URL = 'https://api.example.com';

// main.ts - Different import styles

// 1. Type-only import (TS 3.8+)
import type { User } from './types';
// Removed at runtime, only for type checking

// 2. Mixed import (TS 4.5+ inline style)
import { type User, UserService, API_URL } from './types';
// User is type-only, UserService and API_URL are values

// 3. Separate imports (clear separation)
import type { User } from './types';
import { UserService, API_URL } from './types';

// 4. Type-only export
// api.ts
export type { User } from './types';  // Re-export only type
export { UserService } from './types';  // Re-export value

// Benefits:
// - Smaller bundles (types removed)
// - Clear intent (type vs value)
// - Better tree shaking
// - Required for isolatedModules

// tsconfig.json
{
  "compilerOptions": {
    // Enforce type-only imports where possible
    "verbatimModuleSyntax": true,  // TS 5.0+ (replaces below)
    
    // Or legacy options:
    "importsNotUsedAsValues": "error",
    "preserveValueImports": false,
    "isolatedModules": true
  }
}

// ESLint rule to enforce
{
  "rules": {
    "@typescript-eslint/consistent-type-imports": ["error", {
      "prefer": "type-imports",
      "fixStyle": "inline-type-imports"
    }]
  }
}

Example: Debugging with type imports

// When debugging, type-only imports don't affect runtime

// Before optimization
import { User, ApiClient, ValidationError } from './library';

// User is only used for types
function processUser(user: User) { }

// After optimization
import type { User } from './library';  // Type-only
import { ApiClient, ValidationError } from './library';  // Runtime values

function processUser(user: User) { }  // User type available at compile time

// Helps identify dead code
// If you see a regular import only used for types,
// convert it to type-only import

// Example: Debugging circular dependencies
// file-a.ts
import type { TypeB } from './file-b';  // Safe - type-only
export interface TypeA {
  b: TypeB;
}

// file-b.ts
import type { TypeA } from './file-a';  // Safe - type-only
export interface TypeB {
  a: TypeA;
}

// Type-only imports don't cause runtime circular dependency

// Debugging bundle size
// Before:
import { LargeLibrary } from 'some-package';
type Config = LargeLibrary.Config;  // Imports entire library!

// After:
import type { Config } from 'some-package';  // Only type, no runtime import

// Use source-map-explorer to verify:
npm run build
source-map-explorer dist/*.js

18.6 Conditional Compilation and Environment Types

Technique Implementation Purpose Use Case
Environment Variables process.env.NODE_ENV with types Different behavior per environment Development vs production features
Type Declarations Declare global types for env variables Type safety for process.env Avoid runtime undefined errors
Const Assertions const config = { ... } as const Narrow types to literal values Type-safe configuration
Dead Code Elimination Bundler removes unreachable code Smaller production bundles Debug logging, dev tools
Multiple tsconfig Different configs per environment Different types/settings Test vs production builds
Conditional Types Types based on environment Different APIs per environment Mock vs real implementations
Feature Flags Type-safe feature toggles Enable/disable features safely A/B testing, gradual rollouts

Example: Environment-specific types

// env.d.ts - Type environment variables
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV: 'development' | 'production' | 'test';
      API_URL: string;
      DEBUG?: 'true' | 'false';
      DATABASE_URL: string;
      PORT?: string;
    }
  }
}

export {};

// Now process.env is fully typed
const apiUrl: string = process.env.API_URL;  // Type-safe
const port: string = process.env.PORT ?? '3000';

// Conditional compilation with dead code elimination
const isDevelopment = process.env.NODE_ENV === 'development';

if (isDevelopment) {
  // This code is removed in production builds by bundlers
  console.log('Debug mode enabled');
  enableDevTools();
}

// Better: Use function for tree-shaking
function debug(message: string) {
  if (process.env.NODE_ENV === 'development') {
    console.log('[DEBUG]', message);
  }
}

// Even better: Replace at build time
// webpack.DefinePlugin or Vite define
const DEBUG = __DEV__;  // Replaced with true/false at build time

if (DEBUG) {
  console.log('Development mode');
}

// Type-safe config based on environment
type Config<T extends 'development' | 'production'> = T extends 'development'
  ? { debug: true; logLevel: 'verbose'; apiUrl: string }
  : { debug: false; logLevel: 'error'; apiUrl: string; cdnUrl: string };

function getConfig<T extends 'development' | 'production'>(
  env: T
): Config<T> {
  if (env === 'development') {
    return {
      debug: true,
      logLevel: 'verbose',
      apiUrl: 'http://localhost:3000'
    } as Config<T>;
  } else {
    return {
      debug: false,
      logLevel: 'error',
      apiUrl: 'https://api.example.com',
      cdnUrl: 'https://cdn.example.com'
    } as Config<T>;
  }
}

const devConfig = getConfig('development');
devConfig.debug;  // true (type-safe)

Example: Feature flags and conditional types

// Feature flag system with type safety
type FeatureFlags = {
  newUI: boolean;
  betaFeatures: boolean;
  experimentalAPI: boolean;
};

const flags: FeatureFlags = {
  newUI: process.env.ENABLE_NEW_UI === 'true',
  betaFeatures: process.env.NODE_ENV === 'development',
  experimentalAPI: false
};

// Type-safe feature check
function isFeatureEnabled<K extends keyof FeatureFlags>(
  feature: K
): boolean {
  return flags[feature];
}

// Conditional API based on flags
type API<T extends FeatureFlags> = T['experimentalAPI'] extends true
  ? ExperimentalAPI
  : StableAPI;

interface StableAPI {
  fetch(url: string): Promise<Response>;
}

interface ExperimentalAPI extends StableAPI {
  fetchWithCache(url: string): Promise<Response>;
  preload(urls: string[]): Promise<void>;
}

// Multiple tsconfig for different environments
// tsconfig.base.json - Shared config
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true
  }
}

// tsconfig.json - Development
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "sourceMap": true,
    "declaration": true,
    "types": ["node", "jest"]
  },
  "include": ["src/**/*", "test/**/*"]
}

// tsconfig.prod.json - Production
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "sourceMap": false,
    "removeComments": true,
    "declaration": false
  },
  "include": ["src/**/*"],
  "exclude": ["test/**/*", "**/*.test.ts"]
}

// Build scripts
{
  "scripts": {
    "build:dev": "tsc",
    "build:prod": "tsc -p tsconfig.prod.json"
  }
}

// Webpack/Vite define plugin
// vite.config.ts
export default defineConfig({
  define: {
    '__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'),
    '__VERSION__': JSON.stringify(process.env.npm_package_version),
    'process.env.API_URL': JSON.stringify(process.env.API_URL)
  }
});

// globals.d.ts
declare const __DEV__: boolean;
declare const __VERSION__: string;

// Usage - dead code eliminated in production
if (__DEV__) {
  console.log(`Running version ${__VERSION__}`);
  enableDebugTools();
}
Note: Error handling and debugging best practices:
  • Compiler errors - Read messages carefully, use hover for type info, check error codes
  • Diagnosis - Break down complex types, use type annotations, @ts-expect-error for known issues
  • Source maps - Enable for debugging, use declarationMap for libraries, configure VS Code launch.json
  • Runtime errors - Use Result types, custom error classes with type guards, validation libraries
  • Type imports - Use type-only imports for smaller bundles, enable verbatimModuleSyntax
  • Environment - Type process.env, use feature flags, conditional compilation for optimizations

Error Handling and Debugging Summary

  • Compiler errors - Understand common error codes (TS2322, TS2339, TS2571), read full messages
  • Diagnosis - Use hover, go to definition, type annotations, isolation techniques to debug type errors
  • Source maps - Enable sourceMap and declarationMap, configure VS Code for debugging TypeScript
  • Runtime safety - Result types, custom errors with type guards, validation libraries (Zod, io-ts)
  • Type imports - Use import type for compile-time only imports, smaller bundles, clearer intent
  • Environments - Type process.env, use feature flags, multiple tsconfig files, dead code elimination

19. Advanced Patterns and Enterprise Features

19.1 Dependency Injection and IoC Container Typing

Library/Pattern Approach Type Safety Use Case
InversifyJS Decorator-based DI container - @injectable, @inject Full type inference, constructor injection Enterprise apps, testable architecture
TSyringe Lightweight DI - Microsoft library Decorator metadata, automatic resolution Node.js services, Clean Architecture
TypeDI Dependency injection for TypeScript/JavaScript Service tokens, type-safe containers TypeORM integration, modular apps
Manual DI Pattern Constructor injection without container Explicit types, no magic Simple apps, avoid framework lock-in
Factory Pattern Type-safe factories with generics Return type inference Object creation, polymorphism
Service Locator Central registry for dependencies Type-safe get methods Legacy integration, runtime resolution

Example: InversifyJS dependency injection

// Install: npm i inversify reflect-metadata

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

// types.ts - Define service identifiers
export const TYPES = {
  Database: Symbol.for('Database'),
  Logger: Symbol.for('Logger'),
  UserService: Symbol.for('UserService')
};

// interfaces.ts
export interface ILogger {
  log(message: string): void;
}

export interface IDatabase {
  query(sql: string): Promise<any>;
}

export interface IUserService {
  getUser(id: string): Promise<User>;
}

// implementations.ts
import { injectable, inject } from 'inversify';
import 'reflect-metadata';

@injectable()
export class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

@injectable()
export class PostgresDatabase implements IDatabase {
  async query(sql: string): Promise<any> {
    // Database implementation
    return [];
  }
}

@injectable()
export class UserService implements IUserService {
  constructor(
    @inject(TYPES.Database) private database: IDatabase,
    @inject(TYPES.Logger) private logger: ILogger
  ) {}

  async getUser(id: string): Promise<User> {
    this.logger.log(`Fetching user ${id}`);
    const result = await this.database.query(
      `SELECT * FROM users WHERE id = '${id}'`
    );
    return result[0];
  }
}

// container.ts - Configure DI container
import { Container } from 'inversify';

const container = new Container();

container.bind<ILogger>(TYPES.Logger).to(ConsoleLogger);
container.bind<IDatabase>(TYPES.Database).to(PostgresDatabase);
container.bind<IUserService>(TYPES.UserService).to(UserService);

export { container };

// Usage
import { container } from './container';

const userService = container.get<IUserService>(TYPES.UserService);
const user = await userService.getUser('123');  // Fully typed!

// Testing - easy to mock dependencies
const mockLogger: ILogger = { log: jest.fn() };
const mockDatabase: IDatabase = { query: jest.fn() };

const testContainer = new Container();
testContainer.bind<ILogger>(TYPES.Logger).toConstantValue(mockLogger);
testContainer.bind<IDatabase>(TYPES.Database).toConstantValue(mockDatabase);

Example: Manual DI pattern (no framework)

// Simple, explicit dependency injection

// Services
interface Logger {
  log(message: string): void;
}

interface Database {
  query<T>(sql: string): Promise<T[]>;
}

class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}

class PostgresDB implements Database {
  async query<T>(sql: string): Promise<T[]> {
    // Implementation
    return [] as T[];
  }
}

// Service with constructor injection
class UserService {
  constructor(
    private readonly db: Database,
    private readonly logger: Logger
  ) {}

  async getUser(id: string): Promise<User> {
    this.logger.log(`Fetching user ${id}`);
    const [user] = await this.db.query<User>(
      `SELECT * FROM users WHERE id = $1`
    );
    return user;
  }
}

// Manual wiring (composition root)
function createServices() {
  const logger = new ConsoleLogger();
  const database = new PostgresDB();
  const userService = new UserService(database, logger);
  
  return {
    logger,
    database,
    userService
  };
}

// Usage
const services = createServices();
const user = await services.userService.getUser('123');

// Testing - inject mocks
const mockDB: Database = {
  query: jest.fn().mockResolvedValue([{ id: '123', name: 'Alice' }])
};
const mockLogger: Logger = { log: jest.fn() };
const testService = new UserService(mockDB, mockLogger);

// Type-safe factory pattern
type ServiceFactory<T> = () => T;

class ServiceContainer {
  private factories = new Map<string, ServiceFactory<any>>();
  private instances = new Map<string, any>();

  register<T>(name: string, factory: ServiceFactory<T>): void {
    this.factories.set(name, factory);
  }

  get<T>(name: string): T {
    if (!this.instances.has(name)) {
      const factory = this.factories.get(name);
      if (!factory) throw new Error(`Service ${name} not registered`);
      this.instances.set(name, factory());
    }
    return this.instances.get(name) as T;
  }
}

// Usage
const container = new ServiceContainer();
container.register<Logger>('logger', () => new ConsoleLogger());
container.register<Database>('database', () => new PostgresDB());
container.register<UserService>(
  'userService',
  () => new UserService(
    container.get<Database>('database'),
    container.get<Logger>('logger')
  )
);

const service = container.get<UserService>('userService');

19.2 Plugin Architecture and Type-safe APIs

Pattern Implementation Type Safety Benefit
Plugin Interface Define contract for all plugins Enforce plugin structure Consistent plugin API
Hook System Type-safe event hooks with generics Typed hook parameters and returns Extensibility without modification
Plugin Registry Type-safe plugin registration and discovery Infer plugin types automatically Dynamic plugin loading
Middleware Pattern Composable middleware chain Type-safe context passing Request/response transformation
Extension Points Predefined places for extension Typed extension contracts Open-closed principle
Builder Pattern Fluent API with type constraints Type-safe method chaining Progressive configuration

Example: Type-safe plugin system

// Define plugin interface
interface Plugin<TConfig = unknown> {
  name: string;
  version: string;
  config?: TConfig;
  
  // Lifecycle hooks
  init?(context: PluginContext): void | Promise<void>;
  beforeRequest?(req: Request): Request | Promise<Request>;
  afterResponse?(res: Response): Response | Promise<Response>;
  destroy?(): void | Promise<void>;
}

// Plugin context with typed services
interface PluginContext {
  logger: Logger;
  config: AppConfig;
  registerHook<T>(name: string, handler: HookHandler<T>): void;
}

// Hook system with generics
type HookHandler<T> = (data: T) => T | Promise<T>;

class HookManager {
  private hooks = new Map<string, HookHandler<any>[]>();

  register<T>(name: string, handler: HookHandler<T>): void {
    const handlers = this.hooks.get(name) ?? [];
    handlers.push(handler);
    this.hooks.set(name, handlers);
  }

  async execute<T>(name: string, data: T): Promise<T> {
    const handlers = this.hooks.get(name) ?? [];
    let result = data;
    for (const handler of handlers) {
      result = await handler(result);
    }
    return result;
  }
}

// Plugin registry with type inference
class PluginRegistry {
  private plugins = new Map<string, Plugin>();

  register<T extends Plugin>(plugin: T): void {
    this.plugins.set(plugin.name, plugin);
  }

  get<T extends Plugin>(name: string): T | undefined {
    return this.plugins.get(name) as T | undefined;
  }

  getAll(): Plugin[] {
    return Array.from(this.plugins.values());
  }
}

// Example plugins
interface LoggingPluginConfig {
  level: 'info' | 'debug' | 'error';
  format: 'json' | 'text';
}

class LoggingPlugin implements Plugin<LoggingPluginConfig> {
  name = 'logging';
  version = '1.0.0';
  
  constructor(public config: LoggingPluginConfig) {}

  init(context: PluginContext): void {
    context.logger.log(`Logging plugin initialized with level: ${this.config.level}`);
  }

  async beforeRequest(req: Request): Promise<Request> {
    console.log(`[${this.config.level}] Request: ${req.url}`);
    return req;
  }
}

interface CachePluginConfig {
  ttl: number;
  maxSize: number;
}

class CachePlugin implements Plugin<CachePluginConfig> {
  name = 'cache';
  version = '1.0.0';
  private cache = new Map<string, any>();

  constructor(public config: CachePluginConfig) {}

  async afterResponse(res: Response): Promise<Response> {
    // Cache logic
    return res;
  }
}

// Application with plugins
class Application {
  private registry = new PluginRegistry();
  private hooks = new HookManager();

  use<T extends Plugin>(plugin: T): this {
    this.registry.register(plugin);
    
    const context: PluginContext = {
      logger: console,
      config: {},
      registerHook: (name, handler) => this.hooks.register(name, handler)
    };

    plugin.init?.(context);
    return this;  // Fluent API
  }

  async handleRequest(req: Request): Promise<Response> {
    // Execute beforeRequest hooks
    const modifiedReq = await this.hooks.execute('beforeRequest', req);
    
    // Process request
    const response = await this.processRequest(modifiedReq);
    
    // Execute afterResponse hooks
    return this.hooks.execute('afterResponse', response);
  }

  private async processRequest(req: Request): Promise<Response> {
    // Implementation
    return new Response();
  }
}

// Usage - fully typed!
const app = new Application()
  .use(new LoggingPlugin({ level: 'debug', format: 'json' }))
  .use(new CachePlugin({ ttl: 3600, maxSize: 100 }));

await app.handleRequest(new Request('https://api.example.com'));

19.3 Schema Validation Integration (Zod, Joi)

Library Features Type Inference Use Case
Zod TypeScript-first, composable, no dependencies Infer types from schemas - z.infer<typeof schema> API validation, form validation, config parsing
Joi Mature, extensive validators, plugins Manual type definitions or joi-to-typescript Node.js APIs, configuration validation
Yup Similar to Joi, smaller bundle InferType<typeof schema> utility Form validation (React Hook Form, Formik)
io-ts Functional programming style, codec pattern Runtime + compile-time validation Functional codebases, API boundaries
class-validator Decorator-based validation Works with TypeScript classes NestJS, class-based DTOs
ArkType Ultra-fast, TypeScript syntax 1:1 TypeScript to runtime validation Performance-critical validation

Example: Zod schema validation

// Install: npm i zod
import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.date(),
  metadata: z.record(z.unknown()).optional()
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// Equivalent to:
// type User = {
//   id: string;
//   name: string;
//   email: string;
//   age?: number | undefined;
//   role: 'admin' | 'user' | 'guest';
//   createdAt: Date;
//   metadata?: Record<string, unknown> | undefined;
// }

// Validation
function createUser(data: unknown): User {
  // Parse and validate - throws on error
  return UserSchema.parse(data);
}

// Safe parsing - returns result object
function safeCreateUser(data: unknown): User | null {
  const result = UserSchema.safeParse(data);
  if (result.success) {
    return result.data;  // Typed as User
  } else {
    console.error(result.error.issues);
    return null;
  }
}

// Nested schemas
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string().regex(/^\d{5}$/),
  country: z.string().length(2)  // ISO country code
});

const UserWithAddressSchema = UserSchema.extend({
  address: AddressSchema,
  billingAddress: AddressSchema.optional()
});

type UserWithAddress = z.infer<typeof UserWithAddressSchema>;

// Array and union schemas
const UsersSchema = z.array(UserSchema);
type Users = z.infer<typeof UsersSchema>;  // User[]

const StringOrNumberSchema = z.union([z.string(), z.number()]);
type StringOrNumber = z.infer<typeof StringOrNumberSchema>;  // string | number

// Refinements and custom validation
const PasswordSchema = z.string()
  .min(8, 'Password must be at least 8 characters')
  .regex(/[A-Z]/, 'Password must contain uppercase letter')
  .regex(/[0-9]/, 'Password must contain number');

const ConfirmPasswordSchema = z.object({
  password: PasswordSchema,
  confirmPassword: z.string()
}).refine(
  (data) => data.password === data.confirmPassword,
  { message: 'Passwords must match', path: ['confirmPassword'] }
);

// Transform data
const DateStringSchema = z.string().transform((str) => new Date(str));
const trimmedString = z.string().transform((s) => s.trim());

// API endpoint with validation
import { Request, Response } from 'express';

const CreateUserRequestSchema = z.object({
  body: UserSchema.omit({ id: true, createdAt: true }),
  query: z.object({
    sendEmail: z.string().transform(s => s === 'true').optional()
  })
});

async function createUserHandler(req: Request, res: Response) {
  try {
    const { body, query } = CreateUserRequestSchema.parse({
      body: req.body,
      query: req.query
    });
    
    // body and query are fully typed!
    const user = await saveUser(body);
    
    if (query.sendEmail) {
      await sendWelcomeEmail(user.email);
    }
    
    res.json(user);
  } catch (error) {
    if (error instanceof z.ZodError) {
      res.status(400).json({ errors: error.issues });
    } else {
      res.status(500).json({ error: 'Internal server error' });
    }
  }
}

Example: Joi validation

// Install: npm i joi @types/joi
import Joi from 'joi';

// Define schema
const userSchema = Joi.object({
  id: Joi.string().uuid().required(),
  name: Joi.string().min(1).max(100).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().positive().optional(),
  role: Joi.string().valid('admin', 'user', 'guest').required(),
  createdAt: Joi.date().required(),
  metadata: Joi.object().unknown(true).optional()
});

// Manual type definition (or use joi-to-typescript)
interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
  role: 'admin' | 'user' | 'guest';
  createdAt: Date;
  metadata?: Record<string, unknown>;
}

// Validation
function validateUser(data: unknown): User {
  const { error, value } = userSchema.validate(data, {
    abortEarly: false,  // Return all errors
    stripUnknown: true  // Remove unknown properties
  });

  if (error) {
    throw new Error(`Validation failed: ${error.message}`);
  }

  return value as User;
}

// Express middleware for validation
function validate(schema: Joi.Schema) {
  return (req: Request, res: Response, next: Function) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false
    });

    if (error) {
      const errors = error.details.map(d => ({
        field: d.path.join('.'),
        message: d.message
      }));
      return res.status(400).json({ errors });
    }

    req.body = value;  // Replace with validated data
    next();
  };
}

// Usage
app.post('/users', validate(userSchema), createUserHandler);

19.4 ORM Integration (Prisma, TypeORM) Type Safety

ORM Type Generation Query Builder Features
Prisma Generate types from schema - prisma generate Fluent API, full type inference Best-in-class types, migrations, Prisma Studio
TypeORM Decorators on entities - infer from classes Repository pattern, QueryBuilder Mature, Active Record/Data Mapper patterns
Drizzle ORM TypeScript-first, schema definition SQL-like builder with full types Lightweight, edge-compatible, serverless
Kysely Type-safe SQL query builder Composable queries with inference Raw SQL control with type safety
MikroORM Entity classes with decorators Unit of Work, Identity Map DDD patterns, complex relationships

Example: Prisma type-safe ORM

// Install: npm i @prisma/client
// Dev: npm i -D prisma

// Initialize Prisma
// npx prisma init

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String
  role      Role     @default(USER)
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        String   @id @default(uuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  tags      Tag[]
  createdAt DateTime @default(now())
}

model Profile {
  id     String @id @default(uuid())
  bio    String
  userId String @unique
  user   User   @relation(fields: [userId], references: [id])
}

model Tag {
  id    String @id @default(uuid())
  name  String @unique
  posts Post[]
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

// Generate Prisma Client
// npx prisma generate

// Usage - fully typed!
import { PrismaClient, Prisma } from '@prisma/client';

const prisma = new PrismaClient();

// Type-safe queries
async function getUser(id: string) {
  const user = await prisma.user.findUnique({
    where: { id },
    include: {
      posts: true,
      profile: true
    }
  });
  // user type is inferred: User & { posts: Post[], profile: Profile | null }
  return user;
}

// Type-safe create
async function createUser(data: Prisma.UserCreateInput) {
  return prisma.user.create({
    data: {
      email: data.email,
      name: data.name,
      profile: {
        create: {
          bio: 'New user'
        }
      }
    }
  });
}

// Type-safe where conditions
async function findUsers(filter: Prisma.UserWhereInput) {
  return prisma.user.findMany({
    where: filter,
    orderBy: { createdAt: 'desc' }
  });
}

// Usage
const admins = await findUsers({ role: 'ADMIN' });  // Fully typed!

// Type-safe updates
await prisma.user.update({
  where: { id: '123' },
  data: {
    name: 'New Name',
    posts: {
      create: {
        title: 'New Post',
        content: 'Content here'
      }
    }
  }
});

// Aggregations with types
const stats = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { authorId: false },
  where: { published: true }
});
// stats is fully typed

// Transactions
await prisma.$transaction([
  prisma.user.create({ data: { email: 'user@example.com', name: 'User' } }),
  prisma.post.create({ data: { title: 'Post', authorId: '...' } })
]);

// Raw queries with types
const users = await prisma.$queryRaw<User[]>`
  SELECT * FROM "User" WHERE role = ${Prisma.Role.ADMIN}
`;

Example: TypeORM entities

// Install: npm i typeorm reflect-metadata
// Database driver: npm i pg (for PostgreSQL)

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

// entities/User.ts
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
  OneToOne
} from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column({ unique: true })
  email!: string;

  @Column()
  name!: string;

  @Column({ type: 'enum', enum: ['USER', 'ADMIN', 'MODERATOR'], default: 'USER' })
  role!: 'USER' | 'ADMIN' | 'MODERATOR';

  @OneToMany(() => Post, post => post.author)
  posts!: Post[];

  @OneToOne(() => Profile, profile => profile.user)
  profile?: Profile;

  @CreateDateColumn()
  createdAt!: Date;

  @UpdateDateColumn()
  updatedAt!: Date;
}

// entities/Post.ts
@Entity()
export class Post {
  @PrimaryGeneratedColumn('uuid')
  id!: string;

  @Column()
  title!: string;

  @Column({ type: 'text', nullable: true })
  content?: string;

  @Column({ default: false })
  published!: boolean;

  @ManyToOne(() => User, user => user.posts)
  author!: User;

  @CreateDateColumn()
  createdAt!: Date;
}

// data-source.ts
import { DataSource } from 'typeorm';

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: 'localhost',
  port: 5432,
  username: 'user',
  password: 'password',
  database: 'mydb',
  entities: [User, Post, Profile],
  synchronize: true,  // Don't use in production!
  logging: true
});

// Usage
await AppDataSource.initialize();

const userRepository = AppDataSource.getRepository(User);

// Type-safe queries
const user = await userRepository.findOne({
  where: { email: 'user@example.com' },
  relations: ['posts', 'profile']
});

// QueryBuilder with types
const users = await userRepository
  .createQueryBuilder('user')
  .leftJoinAndSelect('user.posts', 'post')
  .where('user.role = :role', { role: 'ADMIN' })
  .andWhere('post.published = :published', { published: true })
  .getMany();

// Type-safe create
const newUser = userRepository.create({
  email: 'new@example.com',
  name: 'New User',
  role: 'USER'
});
await userRepository.save(newUser);

// Transactions
await AppDataSource.transaction(async (manager) => {
  const user = manager.create(User, { email: 'user@example.com', name: 'User' });
  await manager.save(user);
  
  const post = manager.create(Post, { title: 'Post', author: user });
  await manager.save(post);
});

19.5 GraphQL Code Generation and Type Safety

Tool Purpose Output Use Case
GraphQL Code Generator Generate TypeScript from GraphQL schema/operations Types, hooks, resolvers, operations Full-stack GraphQL apps
Apollo Client GraphQL client with TypeScript support useQuery, useMutation hooks with types React/Vue GraphQL frontends
TypeGraphQL Create GraphQL schema using TypeScript classes Schema-first from decorators TypeScript-first GraphQL APIs
Pothos GraphQL Code-first GraphQL schema builder Type-safe schema construction Type-safe server schema definition
gql-tag Parse GraphQL queries Tagged template literals Client-side query definition
GraphQL ESLint Lint GraphQL operations Catch errors at compile time Query validation

Example: GraphQL Code Generator

// Install: npm i -D @graphql-codegen/cli @graphql-codegen/typescript
// npm i -D @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

// schema.graphql
type User {
  id: ID!
  email: String!
  name: String!
  role: Role!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String
  published: Boolean!
  author: User!
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

type Query {
  user(id: ID!): User
  users(role: Role): [User!]!
  post(id: ID!): Post
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

input CreateUserInput {
  email: String!
  name: String!
  role: Role = USER
}

input UpdateUserInput {
  email: String
  name: String
  role: Role
}

// codegen.yml
overwrite: true
schema: "http://localhost:4000/graphql"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
    config:
      withHooks: true
      withComponent: false

// queries.graphql
query GetUser($id: ID!) {
  user(id: $id) {
    id
    email
    name
    role
    posts {
      id
      title
      published
    }
  }
}

query GetUsers($role: Role) {
  users(role: $role) {
    id
    email
    name
    role
  }
}

mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    email
    name
    role
  }
}

// Generate types
// npx graphql-codegen

// Usage - generated hooks are fully typed!
import { useGetUserQuery, useCreateUserMutation } from './generated/graphql';

function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useGetUserQuery({
    variables: { id: userId }
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  // data.user is fully typed!
  return (
    <div>
      <h1>{data?.user?.name}</h1>
      <p>{data?.user?.email}</p>
      <ul>
        {data?.user?.posts.map(post => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

function CreateUserForm() {
  const [createUser, { loading, error }] = useCreateUserMutation();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    await createUser({
      variables: {
        input: {
          email: 'new@example.com',
          name: 'New User',
          role: 'USER'  // Typed as Role enum!
        }
      }
    });
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

19.6 Microservice Communication and API Contracts

Pattern/Tool Implementation Type Safety Use Case
tRPC End-to-end type-safe APIs without code generation Infer client types from server TypeScript monorepos, full-stack apps
OpenAPI/Swagger API specification with TypeScript generation Generate types from OpenAPI spec REST APIs, polyglot services
gRPC Protocol Buffers with TypeScript Generate from .proto files High-performance microservices
Message Queue Type-safe message schemas (Zod/JSON Schema) Validate messages at runtime Event-driven architecture
API Client Generation Generate SDK from API definition Auto-generated type-safe client External API integration
Contract Testing Pact, MSW for API contracts Type-safe mock servers Consumer-driven contracts

Example: tRPC end-to-end type safety

// Install: npm i @trpc/server @trpc/client @trpc/react-query

// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod';

const t = initTRPC.create();

// Define router
export const appRouter = t.router({
  // Query
  getUser: t.procedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      // Fetch user from database
      return {
        id: input.id,
        name: 'Alice',
        email: 'alice@example.com'
      };
    }),

  // Query with transformed output
  getUsers: t.procedure
    .input(z.object({
      role: z.enum(['admin', 'user']).optional()
    }))
    .query(async ({ input }) => {
      // Return array of users
      return [];
    }),

  // Mutation
  createUser: t.procedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email(),
      role: z.enum(['admin', 'user']).default('user')
    }))
    .mutation(async ({ input }) => {
      // Create user in database
      return {
        id: '123',
        ...input
      };
    }),

  // Nested routers
  posts: t.router({
    list: t.procedure.query(() => []),
    get: t.procedure
      .input(z.object({ id: z.string() }))
      .query(({ input }) => null)
  })
});

export type AppRouter = typeof appRouter;

// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';

createHTTPServer({
  router: appRouter,
  createContext: () => ({})
}).listen(3000);

// client/trpc.ts
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/router';

export const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: 'http://localhost:3000'
    })
  ]
});

// Usage - fully typed!
const user = await trpc.getUser.query({ id: '123' });
// user is typed based on server return type!

const newUser = await trpc.createUser.mutate({
  name: 'Bob',
  email: 'bob@example.com',
  role: 'admin'
});
// Input is validated with Zod, output is typed!

// React integration
import { createTRPCReact } from '@trpc/react-query';

export const trpcReact = createTRPCReact<AppRouter>();

// In component
function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading } = trpcReact.getUser.useQuery({ id: userId });
  
  // data is fully typed!
  return <div>{data?.name}</div>;
}

// Mutation
function CreateUser() {
  const mutation = trpcReact.createUser.useMutation();
  
  const handleSubmit = () => {
    mutation.mutate({
      name: 'Charlie',
      email: 'charlie@example.com'
    });
  };
  
  return <button onClick={handleSubmit}>Create</button>;
}

Example: OpenAPI type generation

// openapi.yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0

paths:
  /users/{id}:
    get:
      operationId: getUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

  /users:
    post:
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
      responses:
        '201':
          description: User created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

components:
  schemas:
    User:
      type: object
      required:
        - id
        - email
        - name
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        role:
          type: string
          enum: [user, admin]

    CreateUserRequest:
      type: object
      required:
        - email
        - name
      properties:
        email:
          type: string
          format: email
        name:
          type: string
        role:
          type: string
          enum: [user, admin]
          default: user

// Install: npm i -D openapi-typescript
// Generate types: npx openapi-typescript openapi.yaml -o src/api.d.ts

// api.d.ts (generated)
export interface paths {
  '/users/{id}': {
    get: operations['getUser'];
  };
  '/users': {
    post: operations['createUser'];
  };
}

export interface components {
  schemas: {
    User: {
      id: string;
      email: string;
      name: string;
      role?: 'user' | 'admin';
    };
    CreateUserRequest: {
      email: string;
      name: string;
      role?: 'user' | 'admin';
    };
  };
}

export interface operations {
  getUser: {
    parameters: { path: { id: string } };
    responses: { 200: { content: { 'application/json': components['schemas']['User'] } } };
  };
  createUser: {
    requestBody: { content: { 'application/json': components['schemas']['CreateUserRequest'] } };
    responses: { 201: { content: { 'application/json': components['schemas']['User'] } } };
  };
}

// Usage with type-safe client
import type { paths } from './api';
import createClient from 'openapi-fetch';

const client = createClient<paths>({ baseUrl: 'https://api.example.com' });

// Fully typed requests and responses!
const { data, error } = await client.GET('/users/{id}', {
  params: { path: { id: '123' } }
});
// data is typed as User

const { data: newUser } = await client.POST('/users', {
  body: {
    email: 'user@example.com',
    name: 'User',
    role: 'admin'
  }
});
// body and data are fully typed!
Note: Enterprise patterns best practices:
  • Dependency Injection - Use InversifyJS or TSyringe for complex apps, manual DI for simplicity
  • Plugins - Define plugin interfaces, type-safe hook system, plugin registry with generics
  • Validation - Use Zod for TypeScript-first validation, infer types from schemas
  • ORMs - Prisma offers best TypeScript experience, TypeORM for mature ecosystem
  • GraphQL - Use GraphQL Code Generator for full type safety, tRPC for TypeScript-only stacks
  • Microservices - tRPC for monorepos, OpenAPI for polyglot, gRPC for performance

Advanced Patterns and Enterprise Features Summary

  • Dependency Injection - InversifyJS/TSyringe for complex DI, manual injection for simple cases
  • Plugin Architecture - Type-safe plugin interfaces, hook systems, registries with generics
  • Schema Validation - Zod for TypeScript-first validation with type inference from schemas
  • ORMs - Prisma (best types), TypeORM (mature), Drizzle (lightweight), all with full type safety
  • GraphQL - Code generation from schema/queries, TypeGraphQL for code-first, full type safety
  • Microservices - tRPC for end-to-end types, OpenAPI generation, gRPC with Protocol Buffers

20. Migration and Adoption Strategies

20.1 JavaScript to TypeScript Migration Patterns

Strategy Approach Effort Best For
Big Bang Convert entire codebase at once High - requires significant time Small projects, greenfield migration
Incremental Convert files gradually, allowJs: true Moderate - spread over time Large codebases, active development
Module-by-Module Convert one module/feature at a time Low per iteration - controlled pace Modular architectures, microservices
Leaf-to-Root Start with files with no dependencies Low initial effort - increases Bottom-up migration, utility libraries
Root-to-Leaf Start with entry points, move down High initial effort - decreases Top-down migration, see benefits early
Test Files First Convert tests before implementation Moderate - tests guide migration Well-tested codebases

Example: Incremental migration setup

// Step 1: Initialize TypeScript in existing project
npm install --save-dev typescript @types/node
npx tsc --init

// Step 2: Configure tsconfig.json for gradual migration
{
  "compilerOptions": {
    // Allow JavaScript files
    "allowJs": true,
    "checkJs": false,  // Don't type-check JS files (yet)
    
    // Output settings
    "outDir": "./dist",
    "rootDir": "./src",
    
    // Start lenient, tighten over time
    "strict": false,
    "noImplicitAny": false,
    "strictNullChecks": false,
    
    // Module settings
    "module": "ESNext",
    "target": "ES2020",
    "moduleResolution": "node",
    "esModuleInterop": true,
    
    // Source maps for debugging
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

// Step 3: Rename files incrementally
// Start with utility files (no dependencies)
mv src/utils/date.js src/utils/date.ts
mv src/utils/string.js src/utils/string.ts

// Step 4: Fix type errors in converted files
// Add explicit types, fix any usage

// Step 5: Gradually enable strict settings
{
  "compilerOptions": {
    "noImplicitAny": true,  // Enable gradually
    // "strictNullChecks": true,  // Enable after most files converted
    // "strict": true  // Final goal
  }
}

// Step 6: Track progress
// Count .ts vs .js files
find src -name "*.ts" | wc -l
find src -name "*.js" | wc -l

// Migration checklist
// ✅ tsconfig.json configured
// ✅ Dependencies have type definitions (@types/*)
// ⏳ 25% of files converted (utils, helpers)
// ⏳ 50% of files converted (components, services)
// ⏳ 75% of files converted (pages, routes)
// ⏳ 100% of files converted
// ⏳ strict: true enabled
// ⏳ All any types replaced

Example: Converting a JavaScript file

// Before: user.js
export function getUser(id) {
  return fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(data => {
      return {
        id: data.id,
        name: data.name,
        email: data.email,
        isActive: data.is_active
      };
    });
}

export function formatUserName(user) {
  return `${user.name} (${user.email})`;
}

export const USER_ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest'
};

// After: user.ts - Step 1 (rename, minimal changes)
export function getUser(id: string): Promise<any> {
  return fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(data => {
      return {
        id: data.id,
        name: data.name,
        email: data.email,
        isActive: data.is_active
      };
    });
}

export function formatUserName(user: any): string {
  return `${user.name} (${user.email})`;
}

export const USER_ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest'
} as const;

// After: user.ts - Step 2 (add proper types)
interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
}

interface ApiUser {
  id: string;
  name: string;
  email: string;
  is_active: boolean;
}

export async function getUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data: ApiUser = await response.json();
  
  return {
    id: data.id,
    name: data.name,
    email: data.email,
    isActive: data.is_active
  };
}

export function formatUserName(user: User): string {
  return `${user.name} (${user.email})`;
}

export const USER_ROLES = {
  ADMIN: 'admin',
  USER: 'user',
  GUEST: 'guest'
} as const;

export type UserRole = typeof USER_ROLES[keyof typeof USER_ROLES];

// After: user.ts - Step 3 (add error handling, validation)
import { z } from 'zod';

const ApiUserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
  is_active: z.boolean()
});

export async function getUser(id: string): Promise<User> {
  if (!id) {
    throw new Error('User ID is required');
  }

  const response = await fetch(`/api/users/${id}`);
  
  if (!response.ok) {
    throw new Error(`Failed to fetch user: ${response.status}`);
  }
  
  const data = await response.json();
  const validated = ApiUserSchema.parse(data);  // Runtime validation
  
  return {
    id: validated.id,
    name: validated.name,
    email: validated.email,
    isActive: validated.is_active
  };
}

20.2 Gradual Type Adoption with @ts-check

Feature Description Usage Benefit
@ts-check Enable TypeScript checking in JavaScript files Add // @ts-check at top of file Type checking without conversion
@ts-nocheck Disable TypeScript checking Add // @ts-nocheck at top Temporarily disable for problematic files
@ts-ignore Ignore error on next line Add // @ts-ignore before line Bypass specific errors
@ts-expect-error Expect error on next line Add // @ts-expect-error Document known issues
JSDoc Types Type annotations in comments /** @type {string} */ Add types without .ts conversion
checkJs Enable type checking for all JS files "checkJs": true in tsconfig Project-wide JS type checking

Example: Using @ts-check with JSDoc

// user.js - JavaScript file with type checking
// @ts-check

/**
 * @typedef {Object} User
 * @property {string} id
 * @property {string} name
 * @property {string} email
 * @property {boolean} isActive
 * @property {'admin' | 'user' | 'guest'} role
 */

/**
 * Fetch a user by ID
 * @param {string} id - User ID
 * @returns {Promise<User>} User data
 */
async function getUser(id) {
  const response = await fetch(`/api/users/${id}`);
  /** @type {User} */
  const user = await response.json();
  return user;
}

/**
 * Format user's display name
 * @param {User} user - User object
 * @returns {string} Formatted name
 */
function formatUserName(user) {
  return `${user.name} (${user.email})`;
}

/**
 * @type {User[]}
 */
const users = [];

// TypeScript will now check these!
const user = await getUser('123');
console.log(formatUserName(user));

// This will show an error!
// @ts-expect-error - role is wrong type
const badUser = { id: '1', name: 'Bad', email: 'bad@example.com', isActive: true, role: 'invalid' };

// Import types from .d.ts or .ts files
/**
 * @param {import('./types').Config} config
 */
function initialize(config) {
  // config is typed based on imported type
}

// Generic types in JSDoc
/**
 * @template T
 * @param {T[]} array
 * @param {(item: T) => boolean} predicate
 * @returns {T | undefined}
 */
function find(array, predicate) {
  return array.find(predicate);
}

// This is type-safe!
const numbers = [1, 2, 3];
const result = find(numbers, n => n > 2);  // result: number | undefined

// Union types
/**
 * @param {string | number} value
 * @returns {string}
 */
function stringify(value) {
  return String(value);
}

// Intersection types (use multiple @typedef)
/**
 * @typedef {Object} HasId
 * @property {string} id
 */

/**
 * @typedef {Object} HasName
 * @property {string} name
 */

/**
 * @typedef {HasId & HasName} Entity
 */

/**
 * @param {Entity} entity
 */
function processEntity(entity) {
  console.log(entity.id, entity.name);  // Both properties available
}

Example: Enabling checkJs project-wide

// tsconfig.json - Type check all JavaScript
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,  // Type check all .js files
    "noEmit": true,   // Don't generate output (just check)
    
    // Be lenient initially
    "strict": false,
    "noImplicitAny": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

// Opt out specific files if needed
// problematic-file.js
// @ts-nocheck
// This file won't be type checked

// Gradually improve coverage
// Run type check
npx tsc --noEmit

// Fix errors one file at a time
// When a file is fully typed, rename to .ts

// package.json scripts
{
  "scripts": {
    "type-check": "tsc --noEmit",
    "type-check:watch": "tsc --noEmit --watch"
  }
}

20.3 Legacy Code Integration Strategies

Strategy Implementation Use Case Trade-off
Type Stubs Create .d.ts files for legacy modules External JS libraries without types Manual maintenance required
any Escape Hatch Use any for unknown legacy types Quick migration, complex legacy code Loses type safety
Wrapper Pattern Wrap legacy code in typed interfaces Isolate legacy code, provide typed API Additional abstraction layer
Adapter Pattern Convert legacy data structures to typed ones Bridge old and new code Runtime conversion overhead
Facade Pattern Type-safe facade over legacy system Complex legacy subsystems Duplicate logic possible
Gradual Rewrite Rewrite modules with proper types Long-term modernization High effort, longer timeline

Example: Creating type stubs for legacy code

// Legacy JavaScript library (can't modify)
// legacy-lib.js
module.exports = {
  createConnection: function(config) {
    // Implementation
    return {
      query: function(sql, params) {
        // Query implementation
      },
      close: function() {
        // Close connection
      }
    };
  },
  utils: {
    escape: function(str) {
      return str.replace(/'/g, "''");
    }
  }
};

// Create type definitions
// legacy-lib.d.ts
export interface Connection {
  query<T = any>(sql: string, params?: any[]): Promise<T[]>;
  close(): Promise<void>;
}

export interface Config {
  host: string;
  port: number;
  username: string;
  password: string;
  database: string;
}

export function createConnection(config: Config): Connection;

export namespace utils {
  export function escape(str: string): string;
}

// Now can use with types!
import * as legacyLib from './legacy-lib';

const connection = legacyLib.createConnection({
  host: 'localhost',
  port: 5432,
  username: 'user',
  password: 'pass',
  database: 'mydb'
});

const users = await connection.query<User>('SELECT * FROM users');

// Wrapper pattern for better API
// db-wrapper.ts
import * as legacyLib from './legacy-lib';

export class Database {
  private connection: legacyLib.Connection;

  constructor(config: legacyLib.Config) {
    this.connection = legacyLib.createConnection(config);
  }

  async query<T>(sql: string, params: any[] = []): Promise<T[]> {
    // Add logging, error handling, retry logic
    console.log('Executing query:', sql);
    try {
      return await this.connection.query<T>(sql, params);
    } catch (error) {
      console.error('Query failed:', error);
      throw error;
    }
  }

  async findOne<T>(table: string, id: string): Promise<T | null> {
    const results = await this.query<T>(
      `SELECT * FROM ${table} WHERE id = $1`,
      [id]
    );
    return results[0] ?? null;
  }

  async close(): Promise<void> {
    await this.connection.close();
  }
}

// Usage with type safety
const db = new Database({ /* config */ });
const user = await db.findOne<User>('users', '123');  // Typed!

Example: Adapter pattern for legacy data

// Legacy API returns different structure
interface LegacyUserResponse {
  user_id: string;
  user_name: string;
  user_email: string;
  is_active: number;  // 0 or 1
  created_date: string;  // Date string
  user_meta: string;  // JSON string
}

// Modern typed structure
interface User {
  id: string;
  name: string;
  email: string;
  isActive: boolean;
  createdAt: Date;
  metadata: Record<string, unknown>;
}

// Adapter to convert legacy to modern
class UserAdapter {
  static fromLegacy(legacy: LegacyUserResponse): User {
    return {
      id: legacy.user_id,
      name: legacy.user_name,
      email: legacy.user_email,
      isActive: legacy.is_active === 1,
      createdAt: new Date(legacy.created_date),
      metadata: legacy.user_meta ? JSON.parse(legacy.user_meta) : {}
    };
  }

  static toLegacy(user: User): LegacyUserResponse {
    return {
      user_id: user.id,
      user_name: user.name,
      user_email: user.email,
      is_active: user.isActive ? 1 : 0,
      created_date: user.createdAt.toISOString(),
      user_meta: JSON.stringify(user.metadata)
    };
  }
}

// Service layer uses modern types
class UserService {
  async getUser(id: string): Promise<User> {
    // Legacy API call
    const response = await fetch(`/api/legacy/users/${id}`);
    const legacyData: LegacyUserResponse = await response.json();
    
    // Convert to modern structure
    return UserAdapter.fromLegacy(legacyData);
  }

  async updateUser(user: User): Promise<User> {
    // Convert to legacy format
    const legacyData = UserAdapter.toLegacy(user);
    
    const response = await fetch(`/api/legacy/users/${user.id}`, {
      method: 'PUT',
      body: JSON.stringify(legacyData)
    });
    
    const updated: LegacyUserResponse = await response.json();
    return UserAdapter.fromLegacy(updated);
  }
}

// Application code only sees modern types
const service = new UserService();
const user = await service.getUser('123');  // User type
console.log(user.isActive);  // boolean, not number!

20.4 Third-party Library Type Definitions

Source Installation Quality Notes
DefinitelyTyped npm i -D @types/library-name Community-maintained, varies Most popular libraries covered
Built-in Types Included with library - check package.json Official, usually excellent Look for "types" field in package.json
Custom .d.ts Create in project: types/library-name/index.d.ts Manual effort required For libraries without types
Declaration Maps Use library's .d.ts.map for source navigation Best debugging experience Jump to actual source code
Type Stubs Minimal types to suppress errors Low - just enough to compile Quick fix, improve later
Module Augmentation Extend existing type definitions Good for plugins/extensions Merge with library types

Example: Working with type definitions

// 1. Install types from DefinitelyTyped
npm install --save-dev @types/lodash
npm install --save-dev @types/express
npm install --save-dev @types/node

// 2. Check if library has built-in types
// Look in package.json
{
  "name": "some-library",
  "types": "./dist/index.d.ts",  // Built-in types!
  "typings": "./dist/index.d.ts"  // Alternative field name
}

// 3. Create custom types for untyped library
// types/untyped-lib/index.d.ts
declare module 'untyped-lib' {
  export function doSomething(param: string): void;
  export class SomeClass {
    constructor(config: any);
    method(): Promise<any>;
  }
}

// 4. Module augmentation to extend library types
// types/express-extended.d.ts
import { User } from './models';

declare global {
  namespace Express {
    interface Request {
      user?: User;  // Add custom property
      sessionId?: string;
    }
  }
}

// Now can use extended types
import { Request, Response } from 'express';

app.get('/profile', (req: Request, res: Response) => {
  if (req.user) {  // TypeScript knows about req.user!
    res.json(req.user);
  }
});

// 5. Ambient module declarations for non-JS assets
// types/assets.d.ts
declare module '*.png' {
  const value: string;
  export default value;
}

declare module '*.svg' {
  const content: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
  export default content;
}

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

// Now can import assets with types
import logo from './logo.png';  // string
import Icon from './icon.svg';  // React component
import styles from './styles.css';  // object

// 6. Configure TypeScript to find custom types
// tsconfig.json
{
  "compilerOptions": {
    "typeRoots": [
      "./node_modules/@types",
      "./types"  // Custom types directory
    ],
    "types": [
      "node",
      "jest",
      "express"
    ]
  }
}

// 7. Handling libraries with wrong/outdated types
// Use @ts-expect-error with comment
import { someFunction } from 'badly-typed-lib';

// @ts-expect-error - Library types are incorrect, see issue #123
const result = someFunction('param');

20.5 Team Onboarding and Training Approaches

Activity Duration Focus Outcome
Fundamentals Workshop Half day - 1 day Basic types, interfaces, generics Team can write typed code
Code Review Sessions Ongoing - 30min weekly Review TypeScript PRs, discuss patterns Consistent code quality
Pair Programming Ongoing - 2-4hrs weekly Senior with junior, solve real problems Hands-on learning
Style Guide Creation 1 week Document team conventions Consistency across codebase
Migration Pairing Ongoing - during migration Convert files together, teach patterns Faster migration, knowledge sharing
Brown Bag Sessions Monthly - 30-45min Advanced topics, new features, tips Continuous improvement

Example: Onboarding checklist

// TypeScript Onboarding Checklist

// Week 1: Setup & Basics
☐ Install VS Code with TypeScript extensions
  - ESLint, Prettier, Error Lens
☐ Clone team repository with TypeScript config
☐ Run `npm install` and `npm run type-check`
☐ Complete basic TypeScript tutorial
  - Types, interfaces, functions
☐ Read team style guide
☐ Set up dev environment (tsconfig, linting)

// Week 2: Reading & Understanding
☐ Review 5 existing TypeScript files in codebase
☐ Understand common patterns used by team
☐ Shadow senior developer during code review
☐ Ask questions about unfamiliar patterns
☐ Read documentation on utility types

// Week 3: Small Contributions
☐ Convert 1-2 simple JavaScript files to TypeScript
☐ Submit PR with proper types
☐ Respond to code review feedback
☐ Fix type errors in existing code
☐ Add JSDoc types to JavaScript files

// Week 4: Independent Work
☐ Implement new feature with TypeScript
☐ Write tests with proper types
☐ Review others' TypeScript PRs
☐ Refactor code to use better types
☐ Share learnings with team

// Ongoing Learning
☐ Attend weekly TypeScript office hours
☐ Contribute to team style guide
☐ Learn advanced patterns (generics, utility types)
☐ Stay updated with TypeScript releases
☐ Help onboard new team members

Example: Team training resources

// Team TypeScript Learning Path

// 1. Official Documentation
// https://www.typescriptlang.org/docs/handbook/intro.html
// Comprehensive, authoritative source

// 2. Team Wiki / Confluence
// Document team-specific patterns and conventions
// Examples:
// - How we handle API types
// - Common utility types we use
// - Error handling patterns
// - Testing conventions

// 3. Internal Code Examples
// Create examples repository with common patterns

// /examples/basic-types.ts
interface User {
  id: string;
  email: string;
  name: string;
}

// /examples/generics.ts
function createResponse<T>(data: T): ApiResponse<T> {
  return { success: true, data };
}

// /examples/utility-types.ts
type UpdateUserInput = Partial<Omit<User, 'id'>>;

// 4. Code Review Guidelines
// - Always provide types for function parameters and returns
// - Prefer interfaces over type aliases for object shapes
// - Use unknown over any when type is truly unknown
// - Document complex types with JSDoc
// - Use utility types instead of manual type manipulation

// 5. Weekly Office Hours
// Every Friday 2-3pm: TypeScript Q&A
// - Bring questions about types you're stuck on
// - Discuss challenging patterns
// - Review recent TypeScript changes to codebase

// 6. Slack Channel: #typescript-help
// - Ask questions
// - Share tips and articles
// - Discuss TypeScript updates

// 7. Recommended Courses
// - TypeScript Deep Dive (free book)
// - Execute Program: TypeScript
// - Frontend Masters: TypeScript courses
// - Total TypeScript by Matt Pocock

// 8. Practice Exercises
// Type Challenges: https://github.com/type-challenges/type-challenges
// Start with easy, progress to hard

// 9. Lunch & Learn Sessions
// Monthly presentations:
// - "Advanced Generic Patterns"
// - "Type-safe API Clients"
// - "Migrating Legacy Code"
// - "TypeScript 5.0 New Features"

// 10. Mentorship Program
// Pair junior developers with TypeScript experts
// Regular 1-on-1 sessions to review code and answer questions

20.6 Code Review Guidelines for TypeScript

Category Check For Good Practice Anti-pattern
Type Safety No any, proper types for params/returns Explicit types, unknown over any Excessive any usage, missing types
Type Inference Let TypeScript infer when obvious Infer simple types, annotate complex ones Over-annotation, redundant types
Utility Types Use built-in utilities appropriately Partial<T>, Pick<T, K>, Omit<T, K> Manual type construction when utility exists
Interfaces vs Types Consistent usage per team convention Interface for objects, type for unions/aliases Inconsistent mixing without reason
Null Safety Handle null/undefined properly Optional chaining, nullish coalescing Non-null assertions (!), unchecked access
Type Assertions Minimize use of as keyword Type guards, validation before assertion Unsafe as casts, as any
Documentation JSDoc for public APIs Describe params, returns, examples Missing docs for complex types

Example: Code review checklist

// TypeScript Code Review Checklist

// ✅ Type Safety
// ✓ Good - Explicit types
function getUser(id: string): Promise<User | null> {
  // ...
}

// ✗ Bad - Using any
function getUser(id: any): Promise<any> {
  // ...
}

// ✅ Type Inference
// ✓ Good - Let TypeScript infer
const count = users.length;  // number inferred
const names = users.map(u => u.name);  // string[] inferred

// ✗ Bad - Over-annotation
const count: number = users.length;
const names: string[] = users.map((u: User): string => u.name);

// ✅ Utility Types
// ✓ Good - Use built-in utilities
type UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>;

// ✗ Bad - Manual type construction
type UpdateUserInput = {
  name?: string;
  email?: string;
  age?: number;
  // ... manually list all optional fields
};

// ✅ Null Safety
// ✓ Good - Safe access
const email = user?.profile?.email ?? 'no-email@example.com';

// ✗ Bad - Non-null assertion
const email = user!.profile!.email;  // Can throw at runtime

// ✅ Type Guards vs Assertions
// ✓ Good - Type guard
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'email' in value
  );
}

if (isUser(data)) {
  console.log(data.email);  // Safe
}

// ✗ Bad - Unsafe assertion
const user = data as User;  // No runtime check!
console.log(user.email);

// ✅ Discriminated Unions
// ✓ Good - Proper discriminated union
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

function handle<T>(result: Result<T>) {
  if (result.success) {
    console.log(result.data);  // TypeScript knows data exists
  } else {
    console.error(result.error);  // TypeScript knows error exists
  }
}

// ✗ Bad - Unclear union
type Result<T> = {
  data?: T;
  error?: string;
};

// ✅ Function Overloads
// ✓ Good - Clear overloads
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
function format(value: string | number | Date): string {
  // Implementation
  return String(value);
}

// ✗ Bad - Single signature with union
function format(value: string | number | Date): string {
  return String(value);
}

// ✅ Const Assertions
// ✓ Good - Narrow to literal types
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
} as const;
// type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }

// ✗ Bad - Wider types
const config = {
  apiUrl: 'https://api.example.com',  // type: string
  timeout: 5000  // type: number
};

// ✅ Documentation
// ✓ Good - JSDoc for complex functions
/**
 * Fetch paginated users with optional filtering
 * @param page - Page number (1-indexed)
 * @param pageSize - Number of items per page
 * @param filters - Optional filters for user properties
 * @returns Promise resolving to paginated user data
 * @throws {ApiError} If the API request fails
 * @example
 * const result = await getUsers(1, 10, { role: 'admin' });
 */
async function getUsers(
  page: number,
  pageSize: number,
  filters?: UserFilters
): Promise<PaginatedResponse<User>> {
  // ...
}

// ✗ Bad - No documentation
async function getUsers(page: number, pageSize: number, filters?: UserFilters) {
  // ...
}

Example: PR review feedback examples

// Good Review Feedback

// 1. Constructive type improvement
// ❌ Current:
function processData(data: any) { }

// 💬 Feedback:
// "Can we add a proper type for data? If the structure is unknown,
// consider using 'unknown' and adding a type guard."

// ✅ Suggested:
interface DataInput {
  id: string;
  values: number[];
}
function processData(data: DataInput) { }

// 2. Encourage utility types
// ❌ Current:
type UpdateUser = {
  name?: string;
  email?: string;
  age?: number;
};

// 💬 Feedback:
// "We can use Partial<T> and Omit<T, K> here to derive from User type,
// which will automatically stay in sync if User changes."

// ✅ Suggested:
type UpdateUser = Partial<Omit<User, 'id' | 'createdAt'>>;

// 3. Safer null handling
// ❌ Current:
const userName = user.profile.name;

// 💬 Feedback:
// "Profile might be null/undefined. Consider using optional chaining
// and providing a fallback."

// ✅ Suggested:
const userName = user.profile?.name ?? 'Anonymous';

// 4. Better error types
// ❌ Current:
try {
  await fetchData();
} catch (error) {
  console.error(error.message);  // error is 'any'
}

// 💬 Feedback:
// "Can we add proper typing for the error? Consider creating a type guard."

// ✅ Suggested:
try {
  await fetchData();
} catch (error) {
  if (error instanceof Error) {
    console.error(error.message);
  } else {
    console.error('Unknown error:', error);
  }
}

// 5. Consistent naming
// ❌ Current:
interface IUser { }  // Hungarian notation
type userRole = 'admin' | 'user';  // lowercase

// 💬 Feedback:
// "Let's follow our style guide: PascalCase for types/interfaces,
// no 'I' prefix."

// ✅ Suggested:
interface User { }
type UserRole = 'admin' | 'user';

// Approval comments
// ✅ "Great use of discriminated unions here! Very type-safe."
// ✅ "Nice refactoring to use Pick<T, K>. Much cleaner."
// ✅ "Love the type guards - this makes the code much safer."
// ✅ "Excellent JSDoc documentation!"
Note: Migration and adoption best practices:
  • Migration - Start incremental with allowJs, convert file-by-file, gradually enable strict mode
  • @ts-check - Use JSDoc types in JavaScript for gradual adoption before .ts conversion
  • Legacy code - Create type stubs, use wrapper/adapter patterns, isolate untypes code
  • Type definitions - Install @types/* packages, create custom .d.ts, use module augmentation
  • Onboarding - Workshops, code reviews, pair programming, style guide, continuous learning
  • Code review - Check type safety, avoid any, use utilities, document complex types

Migration and Adoption Strategies Summary

  • JS to TS Migration - Incremental approach with allowJs: true, convert file-by-file, enable strict gradually
  • Gradual Adoption - Use @ts-check and JSDoc in JavaScript files before converting to TypeScript
  • Legacy Integration - Create .d.ts stubs, wrapper/adapter patterns, isolate legacy code with typed APIs
  • Type Definitions - Install @types/* from DefinitelyTyped, check for built-in types, create custom declarations
  • Team Onboarding - Workshops, pair programming, code reviews, style guide, continuous learning culture
  • Code Reviews - Enforce type safety, avoid any, use utility types, document complex patterns, provide constructive feedback