What you’ll learn: Generics (the most powerful TypeScript feature), built-in utility types, type narrowing, and mapped types. These are the concepts you’ll encounter constantly in real TypeScript codebases.


Section 1 β€” Generics

Generics let you write functions and classes that work with any type β€” but still enforce type safety.

The Problem Without Generics

// Without generics β€” you lose type info:
function identity(value: any): any {
    return value;
}
const result = identity("hello");   // result is 'any' β€” TypeScript can't help you!

// Or you'd write a separate function for each type:
function identityString(value: string): string { return value; }
function identityNumber(value: number): number { return value; }
// This doesn't scale!

Generic Functions

// <T> is a type parameter β€” like a variable, but for types
function identity<T>(value: T): T {
    return value;
}

// TypeScript infers T:
const s = identity("hello");    // T = string, s is string
const n = identity(42);         // T = number, n is number
const arr = identity([1, 2, 3]); // T = number[], arr is number[]

// Explicit type argument:
const s2 = identity<string>("hello");   // same result, explicit

Generic with Arrays

// First element of any array type:
function first<T>(arr: T[]): T | undefined {
    return arr[0];
}

first([1, 2, 3]);          // returns number | undefined
first(["a", "b", "c"]);    // returns string | undefined

// Map any array to another type:
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
    return arr.map(fn);
}

mapArray([1, 2, 3], n => n * 2);          // number[]
mapArray(["a", "b"], s => s.length);       // number[]
mapArray([1, 2, 3], n => n.toString());    // string[]

Generic Interfaces

// A response wrapper used in all APIs:
interface ApiResponse<T> {
    data: T;
    success: boolean;
    message?: string;
    timestamp: string;
}

// Use it with any data type:
const fleetResponse: ApiResponse<Fleet> = {
    data: { fleetId: "RIPE-NA", region: "us-east-1" },
    success: true,
    timestamp: new Date().toISOString()
};

const listResponse: ApiResponse<Fleet[]> = {
    data: [fleet1, fleet2],
    success: true,
    timestamp: new Date().toISOString()
};

// A generic repository interface:
interface Repository<T, ID> {
    findById(id: ID): Promise<T | null>;
    save(entity: T): Promise<T>;
    delete(id: ID): Promise<void>;
    findAll(): Promise<T[]>;
}

class FleetRepository implements Repository<Fleet, string> {
    async findById(id: string): Promise<Fleet | null> { /* ... */ }
    async save(fleet: Fleet): Promise<Fleet> { /* ... */ }
    async delete(id: string): Promise<void> { /* ... */ }
    async findAll(): Promise<Fleet[]> { /* ... */ }
}

Generic Constraints

Restrict what types T can be:

// T must have a 'length' property:
function logLength<T extends { length: number }>(value: T): T {
    console.log(`Length: ${value.length}`);
    return value;
}

logLength("hello");      // OK β€” string has length
logLength([1, 2, 3]);    // OK β€” array has length
logLength(42);           // Error! β€” number has no length

// T must be a key of object U:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const fleet = { fleetId: "RIPE-NA", tpm: 50000, region: "us-east-1" };

getProperty(fleet, "fleetId");   // returns string
getProperty(fleet, "tpm");       // returns number
getProperty(fleet, "active");    // Error! 'active' is not a key of fleet

// Multiple constraints:
function merge<T extends object, U extends object>(a: T, b: U): T & U {
    return { ...a, ...b };
}

Section 2 β€” Built-in Utility Types

TypeScript has many built-in helpers to transform types:

Partial<T> β€” all properties optional

interface Fleet {
    fleetId: string;
    region: string;
    tpm: number;
    isActive: boolean;
}

type FleetUpdate = Partial<Fleet>;
// Same as: { fleetId?: string; region?: string; tpm?: number; isActive?: boolean }

// Use case: update functions only need some fields
async function updateFleet(id: string, updates: Partial<Fleet>): Promise<Fleet> {
    const current = await getFleet(id);
    return { ...current, ...updates };
}

updateFleet("RIPE-NA", { tpm: 60000 });          // OK β€” only updating tpm
updateFleet("RIPE-NA", { region: "EU", tpm: 0 }); // OK β€” updating two fields

Required<T> β€” all properties required

type PartialConfig = {
    host?: string;
    port?: number;
    timeout?: number;
};

type FullConfig = Required<PartialConfig>;
// { host: string; port: number; timeout: number }
// All properties are now required

Readonly<T> β€” all properties read-only

interface Config {
    host: string;
    port: number;
}

const config: Readonly<Config> = { host: "localhost", port: 3000 };
config.port = 4000;   // Error! Cannot assign to 'port' because it is a read-only property

// Deep readonly (TypeScript doesn't have this built-in, but you can make it):
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

Record<K, V> β€” object with specific key and value types

// Object where keys are strings and values are numbers:
const tpmByRegion: Record<string, number> = {
    "us-east-1": 50000,
    "eu-west-1": 30000,
    "us-west-2": 20000
};

// Object where keys are from a union type:
type Region = "NA" | "EU" | "FE";
const capacityByRegion: Record<Region, number> = {
    NA: 100,
    EU: 80,
    FE: 60
    // TypeScript will error if you miss any key!
};

Pick<T, K> β€” select subset of properties

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

// Only expose safe fields (no password):
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

function getUserProfile(id: number): Promise<PublicUser> {
    // TypeScript ensures password is never returned
}

Omit<T, K> β€” remove properties

// Same as Pick but you say what to REMOVE:
type PublicUser2 = Omit<User, "password">;
// { id: number; name: string; email: string; createdAt: Date }

// Create input type (no auto-generated fields):
type CreateUserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; password: string }

Exclude<T, U> and Extract<T, U> β€” work on union types

type AllStatus = "pending" | "active" | "completed" | "cancelled" | "archived";

// Remove specific values from union:
type ActiveStatus = Exclude<AllStatus, "archived" | "cancelled">;
// "pending" | "active" | "completed"

// Keep only specific values:
type TerminalStatus = Extract<AllStatus, "completed" | "cancelled" | "archived">;
// "completed" | "cancelled" | "archived"

NonNullable<T> β€” remove null and undefined

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

function processIfExists<T>(value: T | null | undefined): NonNullable<T> | null {
    if (value == null) return null;
    return value as NonNullable<T>;
}

ReturnType<T> and Parameters<T> β€” extract function types

async function getFleet(fleetId: string, eventId: string): Promise<Fleet> { /* ... */ }

type GetFleetReturn = ReturnType<typeof getFleet>;   // Promise<Fleet>
type GetFleetParams = Parameters<typeof getFleet>;    // [string, string]

// Useful when you don't control the function's type:
function wrapWithLogging<T extends (...args: any[]) => any>(
    fn: T
): (...args: Parameters<T>) => ReturnType<T> {
    return function(...args) {
        console.log("Calling with:", args);
        return fn(...args);
    };
}

Awaited<T> β€” unwrap Promise types

type FleetPromise = Promise<Fleet>;
type FleetValue = Awaited<FleetPromise>;   // Fleet (not Promise<Fleet>)

// Useful with ReturnType of async functions:
async function fetchFleet(): Promise<Fleet> { /* ... */ }
type FetchResult = Awaited<ReturnType<typeof fetchFleet>>;   // Fleet

Section 3 β€” Type Narrowing

TypeScript narrows the type inside conditional blocks:

typeof narrowing

function processId(id: string | number): string {
    if (typeof id === "string") {
        return id.toUpperCase();    // TypeScript knows id is string here
    }
    return id.toString();           // TypeScript knows id is number here
}

instanceof narrowing

function handleError(error: Error | string): string {
    if (error instanceof Error) {
        return error.message;   // TypeScript knows it's Error
    }
    return error;               // TypeScript knows it's string
}

in operator narrowing

interface Fleet { fleetId: string; tpm: number; }
interface Service { serviceId: string; email: string; }

function processEntity(entity: Fleet | Service): void {
    if ("fleetId" in entity) {
        // TypeScript knows entity is Fleet here
        console.log(entity.tpm);
    } else {
        // TypeScript knows entity is Service here
        console.log(entity.email);
    }
}

Discriminated Unions

The most powerful pattern β€” add a common property to distinguish types:

type SuccessResult = {
    status: "success";        // discriminant property
    data: Fleet;
};

type ErrorResult = {
    status: "error";          // same property, different value
    error: string;
    code: number;
};

type Result = SuccessResult | ErrorResult;

function handleResult(result: Result): void {
    switch (result.status) {     // switch on the discriminant
        case "success":
            console.log(result.data.fleetId);   // TypeScript knows it's SuccessResult
            break;
        case "error":
            console.log(result.error);           // TypeScript knows it's ErrorResult
            break;
    }
}

Custom Type Guards

// Type guard function β€” returns a boolean AND tells TypeScript the type
function isFleet(entity: unknown): entity is Fleet {
    return (
        typeof entity === "object" &&
        entity !== null &&
        "fleetId" in entity &&
        "tpm" in entity
    );
}

// Usage:
const data: unknown = fetchSomething();

if (isFleet(data)) {
    // TypeScript knows data is Fleet here
    console.log(data.tpm);
}

Exhaustiveness Checking

Ensure every case is handled:

type Shape = "circle" | "rectangle" | "triangle";

function getArea(shape: Shape, ...dims: number[]): number {
    switch (shape) {
        case "circle":    return Math.PI * dims[0] ** 2;
        case "rectangle": return dims[0] * dims[1];
        case "triangle":  return 0.5 * dims[0] * dims[1];
        default:
            // This makes TypeScript error if we add a new Shape but forget to handle it:
            const _exhaustiveCheck: never = shape;
            throw new Error(`Unhandled shape: ${shape}`);
    }
}

Section 4 β€” Mapped Types

Create new types by transforming each property of an existing type:

// Make all properties optional (this is what Partial<T> does):
type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

// Make all properties readonly (this is what Readonly<T> does):
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

// Nullify all properties:
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

type NullableFleet = Nullable<Fleet>;
// { fleetId: string | null; tpm: number | null; ... }

// Custom mapping β€” add a "changed" boolean for each field:
type WithChanges<T> = {
    [K in keyof T]: {
        value: T[K];
        changed: boolean;
    };
};

Section 5 β€” keyof and Indexed Access Types

interface Fleet {
    fleetId: string;
    region: string;
    tpm: number;
    isActive: boolean;
}

// keyof β€” get the union of all property names as string literals
type FleetKey = keyof Fleet;    // "fleetId" | "region" | "tpm" | "isActive"

// Indexed access β€” get the type of a specific property
type FleetTpm = Fleet["tpm"];           // number
type FleetId  = Fleet["fleetId"];        // string

// Combine with keyof:
type FleetValue = Fleet[keyof Fleet];   // string | number | boolean (all possible value types)

// Generic type-safe property getter:
function getFleetProperty<T, K extends keyof T>(fleet: T, key: K): T[K] {
    return fleet[key];    // TypeScript knows the exact return type
}

const id = getFleetProperty(fleet, "fleetId");    // TypeScript knows: string
const tpm = getFleetProperty(fleet, "tpm");       // TypeScript knows: number

Section 6 β€” Template Literal Types

// Combine string literals like template strings:
type EventType = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventType>}`;
// "onClick" | "onFocus" | "onBlur"

// Create API endpoint types:
type Entity = "fleet" | "service" | "event";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Endpoint = `${HttpMethod}:/${Entity}`;
// "GET:/fleet" | "GET:/service" | "GET:/event" | "POST:/fleet" | ...

// Type-safe CSS property names:
type Side = "top" | "right" | "bottom" | "left";
type Spacing = `margin${Capitalize<Side>}`;
// "marginTop" | "marginRight" | "marginBottom" | "marginLeft"

Section 7 β€” Real-World TypeScript Patterns

API client with generics

interface ApiClient {
    get<T>(path: string): Promise<ApiResponse<T>>;
    post<T>(path: string, body: unknown): Promise<ApiResponse<T>>;
    put<T>(path: string, body: unknown): Promise<ApiResponse<T>>;
    delete(path: string): Promise<void>;
}

class HttpClient implements ApiClient {
    constructor(private baseUrl: string) {}

    async get<T>(path: string): Promise<ApiResponse<T>> {
        const response = await fetch(`${this.baseUrl}${path}`);
        return response.json();
    }

    async post<T>(path: string, body: unknown): Promise<ApiResponse<T>> {
        const response = await fetch(`${this.baseUrl}${path}`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body)
        });
        return response.json();
    }
    // ...
}

// Usage β€” TypeScript knows the return types:
const client = new HttpClient("https://api.example.com");
const fleet = await client.get<Fleet>("/fleet/RIPE-NA");     // ApiResponse<Fleet>
const user  = await client.post<User>("/users", newUser);     // ApiResponse<User>

Error handling with typed errors

type AppError =
    | { type: "not_found";      message: string; id: string }
    | { type: "unauthorized";   message: string }
    | { type: "validation";     message: string; fields: string[] }
    | { type: "server_error";   message: string; code: number };

type Result<T> = 
    | { ok: true;  value: T }
    | { ok: false; error: AppError };

async function getFleet(id: string): Promise<Result<Fleet>> {
    try {
        const fleet = await db.find(id);
        if (!fleet) {
            return { ok: false, error: { type: "not_found", message: "Fleet not found", id } };
        }
        return { ok: true, value: fleet };
    } catch (err) {
        return { ok: false, error: { type: "server_error", message: "DB error", code: 500 } };
    }
}

// Caller handles all cases:
const result = await getFleet("RIPE-NA");
if (result.ok) {
    console.log(result.value.tpm);    // TypeScript knows it's Fleet
} else {
    switch (result.error.type) {
        case "not_found":    console.log("ID:", result.error.id); break;
        case "validation":   console.log("Fields:", result.error.fields); break;
        // ...
    }
}

Section 8 β€” Quick Reference

Utility Types:
  Partial<T>            All properties optional
  Required<T>           All properties required
  Readonly<T>           All properties read-only
  Record<K, V>          Object with key type K, value type V
  Pick<T, "a"|"b">      Keep only specified properties
  Omit<T, "a"|"b">      Remove specified properties
  Exclude<T, U>         Remove from union T values that extend U
  Extract<T, U>         Keep from union T values that extend U
  NonNullable<T>        Remove null and undefined
  ReturnType<T>         Return type of function T
  Parameters<T>         Parameter tuple of function T
  Awaited<T>            Unwrap Promise type

Type Narrowing:
  typeof x === "string"    Narrows to string
  instanceof MyClass       Narrows to MyClass
  "key" in object          Narrows to types that have key
  Discriminated union      switch on status/type/kind property
  Custom guard (is)        function x(v: T): v is U

Generics:
  <T>                   Type parameter
  <T extends Foo>       T must extend Foo
  <T, K extends keyof T>  K must be a key of T
  T[K]                  Type of property K in T
  keyof T               Union of all property names