TypeScript Advanced Generics Explained with Examples

Generics in TypeScript provide a way to create reusable and flexible code components by working with a variety of data types. Advanced generics take this concept further by introducing additional features like constraints, default values, and multiple types, which allow developers to write more robust and type-safe code. In this article, examples will be used to explore these advanced concepts in generics.

Generic Constraints

Constraints limit the types that a generic can accept. This ensures that the type passed to a generic function or class meets certain criteria. For example, a constraint can be used to ensure that the generic type has a specific property or method.

function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}

const stringLength = getLength("TypeScript");
const arrayLength = getLength([1, 2, 3]);

In this example, the <T extends { length: number }> constraint ensures that the argument passed to getLength has a length property.

Multiple Generics

TypeScript allows the use of multiple generic types in the same function, class, or interface. This is useful when working with pairs of values or other data structures involving multiple types.

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

const stringNumberPair = pair("TypeScript", 2024);

This function, pair, accepts two different generic types, T and U, and returns a tuple containing both types.

Default Generic Types

Generics in TypeScript can also have default types. This is helpful when you want a generic to have a fallback type if no specific type is provided.

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

const defaultString = identity("Hello");  // T is string
const customNumber = identity<number>(100);  // T is number

In this example, if no type is passed to identity, it defaults to string.

Using Generics with Interfaces

Generics can be used with interfaces to define complex structures where types are not fixed. This adds flexibility to how data is managed.

interface Container<T> {
    value: T;
}

const stringContainer: Container<string> = { value: "Hello" };
const numberContainer: Container<number> = { value: 42 };

The Container interface is designed to hold a value of any type, allowing for different kinds of containers with specific types.

Generic Classes

Classes in TypeScript can also be generic. This is especially useful when designing classes that work with various data types, such as data storage or collection classes.

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

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

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

const stringStore = new DataStore<string>();
stringStore.add("Hello");
stringStore.add("TypeScript");

const numberStore = new DataStore<number>();
numberStore.add(42);

In this example, the DataStore class works with any type of data, providing a type-safe way to store and retrieve elements.

Conclusion

Advanced generics in TypeScript are a powerful tool for writing flexible, reusable, and type-safe code. By using constraints, multiple types, default values, and generics in classes and interfaces, developers can write more complex and robust code. Understanding and utilizing these advanced concepts allows for greater flexibility and ensures type safety across applications.