Generics with Multiple Types in TypeScript: A Complete Guide

Updated: January 7, 2024 By: Guest Contributor Post a comment

Overview

TypeScript’s type system allows developers to build scalable and maintainable applications by providing strong type checks. Generics add another level of flexibility and reusability by making it possible to create components that work with any data type. This tutorial dives deep into the world of generics, focusing on how to use them with multiple types to create versatile and type-safe code.

Introduction to Generics

Generics in TypeScript can be thought of as variables for types. They allow you to define a component that can work over a variety of types rather than a single one. This means you can create functions, interfaces, and classes that can be used with multiple different data types.

function identity<T>(arg: T): T {
    return arg;
}
let output = identity<string>("myString");

Using Multiple Generic Types

While a generic type can stand in for any type, there are times when you may need to operate on two or more types within the same function or class. TypeScript allows you to specify multiple generic types as needed.

function merge<U, V>(obj1: U, obj2: V): U & V {
    return {...obj1, ...obj2};
}
const merged = merge({name: 'John'}, {age: 30});

Constrain Your Generics

Sometimes, you want to impose certain requirements on the types that can be provided as generics. You can use extends to constrain the generic types to a certain shape.

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

Generics in Interfaces

Interfaces in TypeScript can also utilize generics to define methods and properties that can work with any type. This is particularly useful for defining consistent APIs across diverse sets of types.

interface Pair<K, V> {
    key: K;
    value: V;
}
function processPair<K, V>(pair: Pair<K, V>) {
    // process the key and the value
}

Advanced Generic Patterns

As your comfort level with TypeScript generics grows, you can leverage more advanced patterns, such as using generic types in class constructors or implementing generic interfaces.

class Store<S> {
    private _state: S;
    constructor(state: S) {
        this._state = state;
    }
    getState(): S {
        return this._state;
    }
}
interface Computable<A, R> {
    compute(a: A): R;
}
class Calculator implements Computable<number, number> {
    compute(a: number): number {
        return a * a;
    }
}

Generic Constraints and Default Types

You can also provide default types for generics and use multiple constraints to create more precise type conditions.

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

Use of Utility Types with Generics

TypeScript provides several utility types such as Partial, Read-only, and Record that can be combined with generics to facilitate common transformations and manipulations on object properties.

function updateObject<T>(obj: T, partial: Partial<T>): T {
    return {...obj, ...partial};
}

Generic Type Aliases

Type aliases with generics can be used to give a name to a complex generic type, improving code readability and manageability.

type LinkedList<Node> = Node & { next: LinkedList<Node> | null };

Dangers of Excessive Generics

While generics are powerful, overusing them can lead to overly complex and difficult-to-read code. It’s essential to strike a balance and use generics judiciously.

Conclusion

This deep dive into generics with multiple types has unravelled the versatility and power they bring to TypeScript. The proper use of multiple generic types can empower developers to write cleaner, reusable, and type-safe code. As you incorporate these patterns into your work, remember to find the sweet spot where generics enhance rather than complicate your code base.