Generics

Generics in TypeScript provide a way to create reusable components that can work with any data type. They allow you to define functions, classes, and interfaces that work with types specified by the caller, ensuring type safety while providing the flexibility to use different types as needed.

Key Concepts of Generics

Type Variables Generics use type variables to denote types that are specified when the generic is instantiated. The most commonly used type variable is T.

Generic Functions Functions can be made generic to operate on a variety of types while maintaining type safety.

Generic Classes Classes can be defined with generics to handle different types of data in a type-safe manner.

Generic Interfaces Interfaces can use generics to define structures that can work with multiple types.

Generic Functions

A generic function can accept arguments of any type and return a value of that same type.

Example


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

let output1 = identity<string>("myString"); // Explicit type
let output2 = identity<number>(100); // Explicit type
let output3 = identity("myString"); // Type inference

Generic Classes

A generic class can operate on various data types while ensuring type safety.

Example


class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zeroValue: T, addFn: (x: T, y: T) => T) {
        this.zeroValue = zeroValue;
        this.add = addFn;
    }
}

let myGenericNumber = new GenericNumber<number>(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10)); // Output: 15

Generic Interfaces

Interfaces can use generics to describe a structure that can work with multiple types.

Example


interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(5)); // Output: 5

Using Multiple Type Variables

Generics can use multiple type variables to create more complex and flexible components.

Example


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

let mergedObj = merge({ name: "Alice" }, { age: 25 });
console.log(mergedObj); // Output: { name: "Alice", age: 25 }

Generic Constraints

Sometimes you want to limit the kinds of types that can be passed as type arguments. You can use constraints to achieve this.

Example


interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // Now we know it has a .length property, so no error
    return arg;
}

loggingIdentity({ length: 10, value: 3 });

Default Type Parameters

You can provide default types for generics to make the generic components more flexible and easier to use.

Example:


function createArray<T = string>(length: number, value: T): T[] {
    return Array(length).fill(value);
}

let stringArray = createArray(3, "x"); // Output: ["x", "x", "x"]
let numberArray = createArray<number>(3, 5); // Output: [5, 5, 5]