TypeScript Generics

Generics allow writing code that works with multiple types while still maintaining full type safety. Instead of writing the same function or class multiple times for each type, a generic version handles all types with a single definition. The type is specified when the code is used, not when it is written. This makes code reusable, flexible, and type-safe at the same time.

The Problem Generics Solve

  Without generics — repeat the same function for each type:
  function getFirstNumber(arr: number[]): number { return arr[0]; }
  function getFirstString(arr: string[]): string { return arr[0]; }
  function getFirstBoolean(arr: boolean[]): boolean { return arr[0]; }

  With generics — one function handles all types:
  function getFirst<T>(arr: T[]): T { return arr[0]; }

  getFirst([1, 2, 3]);            // T = number → returns number
  getFirst(["a", "b", "c"]);     // T = string → returns string
  getFirst([true, false, true]); // T = boolean → returns boolean

Generic Syntax

  function identity<T>(value: T): T {
                    |         |   |
                    |         |   +-- Return type uses T
                    |         +------ Parameter uses T
                    +---------------- T is the type parameter (placeholder)
  }

The letter T is just a convention (short for "Type"). Any identifier works — U, K, V, or descriptive names like ItemType.

Generic Functions

function identity<T>(value: T): T {
    return value;
}

console.log(identity<string>("Hello"));  // Hello
console.log(identity<number>(42));       // 42
console.log(identity<boolean>(true));    // true

// TypeScript can also infer T from the argument
console.log(identity("TypeScript"));  // T inferred as string
console.log(identity(100));           // T inferred as number

Generic with Arrays

function reverseArray<T>(items: T[]): T[] {
    return items.slice().reverse();
}

let numbers = reverseArray([1, 2, 3, 4, 5]);
console.log(numbers);  // [5, 4, 3, 2, 1]

let names = reverseArray(["Arun", "Bina", "Chetan"]);
console.log(names);    // ["Chetan", "Bina", "Arun"]

Multiple Type Parameters

function pair<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

let item1 = pair("name", "Roshni");   // [string, string]
let item2 = pair(1, "TypeScript");    // [number, string]
let item3 = pair("age", 25);          // [string, number]

console.log(item2); // [1, "TypeScript"]

Generic Interfaces

interface ApiResponse<T> {
    status: number;
    message: string;
    data: T;
}

// Reuse with different data types
let userResponse: ApiResponse<{ name: string; email: string }> = {
    status: 200,
    message: "Success",
    data: { name: "Shilpa", email: "shilpa@mail.com" }
};

let coursesResponse: ApiResponse<string[]> = {
    status: 200,
    message: "Courses loaded",
    data: ["TypeScript", "React", "Node.js"]
};

console.log(userResponse.data.name);       // Shilpa
console.log(coursesResponse.data.length);  // 3

Generic Classes

class DataStore<T> {
    private items: T[] = [];

    add(item: T): void {
        this.items.push(item);
    }

    getAll(): T[] {
        return this.items;
    }

    getById(index: number): T {
        return this.items[index];
    }

    count(): number {
        return this.items.length;
    }
}

// String store
let nameStore = new DataStore<string>();
nameStore.add("Gaurav");
nameStore.add("Hema");
console.log(nameStore.getAll());  // ["Gaurav", "Hema"]

// Number store
let scoreStore = new DataStore<number>();
scoreStore.add(88);
scoreStore.add(95);
scoreStore.add(72);
console.log(scoreStore.count()); // 3

Generic Constraints

A generic constraint limits what types can be used. The extends keyword applies the constraint — the type parameter must extend (be compatible with) the specified type.

// T must have a 'length' property — strings and arrays qualify, numbers do not
function getLength<T extends { length: number }>(item: T): number {
    return item.length;
}

console.log(getLength("Hello"));        // 5 — string has length
console.log(getLength([1, 2, 3, 4]));   // 4 — array has length
console.log(getLength(42));             // Error: number has no 'length' property

keyof Constraint

The keyof operator produces a union of all property names of a type. Using it as a constraint ensures a parameter is a valid key of another type parameter.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

let student = {
    name: "Isha",
    age: 19,
    grade: "A"
};

console.log(getProperty(student, "name"));  // Isha
console.log(getProperty(student, "age"));   // 19
console.log(getProperty(student, "score")); // Error: "score" is not a key of student

Default Type Parameters

interface Container<T = string> {
    value: T;
    label: string;
}

// Uses default type string
let box1: Container = { value: "TypeScript", label: "Language" };

// Explicitly specifies number
let box2: Container<number> = { value: 42, label: "Answer" };

console.log(box1.value.toUpperCase()); // TYPESCRIPT
console.log(box2.value.toFixed(2));    // 42.00

Generic Type Summary

Generic UseSyntaxPurpose
Functionfunction f<T>(x: T): TType-safe reusable function
Interfaceinterface I<T> { data: T }Reusable object shape
Classclass C<T> { items: T[] }Reusable data container
ConstraintT extends SomeTypeLimit what types T can be
keyof constraintK extends keyof TEnsure K is a valid key of T
Default typeT = DefaultTypeFallback type when T is not specified

Practical Example

// Generic pagination helper
interface PaginatedResult<T> {
    items: T[];
    currentPage: number;
    totalPages: number;
    totalItems: number;
    hasNextPage: boolean;
}

function paginate<T>(allItems: T[], page: number, perPage: number): PaginatedResult<T> {
    let start = (page - 1) * perPage;
    let end = start + perPage;
    let items = allItems.slice(start, end);
    let totalPages = Math.ceil(allItems.length / perPage);

    return {
        items,
        currentPage: page,
        totalPages,
        totalItems: allItems.length,
        hasNextPage: page < totalPages
    };
}

let courses = ["TypeScript", "React", "Node.js", "MongoDB", "Express", "GraphQL", "Docker"];

let page1 = paginate(courses, 1, 3);
console.log("Page 1:", page1.items);         // ["TypeScript", "React", "Node.js"]
console.log("Total Pages:", page1.totalPages); // 3
console.log("Has Next:", page1.hasNextPage);   // true

let page3 = paginate(courses, 3, 3);
console.log("Page 3:", page3.items);           // ["Docker"]
console.log("Has Next:", page3.hasNextPage);   // false

Leave a Comment