Generics in TypeScript

Learn how to use Generics in TypeScript with this detailed tutorial. Includes basic and advanced examples to help you master Generics.

Introduction

Generics provide a way to create reusable components that work with a variety of types rather than a single one. They allow you to define functions, classes, and interfaces with a placeholder for types that can be specified when they are used.

Basic Examples

Here are some basic examples of using Generics in TypeScript:

Generic Functions

You can define a generic function using a type parameter.

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

let output1 = identity("myString"); // output1 is of type string
let output2 = identity(42); // output2 is of type number

Generic Interfaces

You can define a generic interface that can be used with different types.

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

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

let myIdentity: GenericIdentityFn = identity;
console.log(myIdentity(10)); // 10

Generic Classes

You can define a generic class that can be used with different types.

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

let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
    return x + y;
};

console.log(myGenericNumber.add(5, 10)); // 15

Generic Constraints

You can constrain the types that can be used with a generic type.

interface Lengthwise {
    length: number;
}

function loggingIdentity(arg: T): T {
    console.log(arg.length);
    return arg;
}

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

Advanced Examples

Here are some advanced examples of using Generics in TypeScript:

Using Class Types in Generics

You can use class types in generics to create more flexible and reusable components.

function create(c: { new(): T }): T {
    return new c();
}

class BeeKeeper {
    hasMask: boolean = true;
}

class ZooKeeper {
    nametag: string = "Mikle";
}

class Animal {
    numLegs: number = 4;
}

class Bee extends Animal {
    keeper: BeeKeeper = new BeeKeeper();
}

class Lion extends Animal {
    keeper: ZooKeeper = new ZooKeeper();
}

function createInstance(c: new () => A): A {
    return new c();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

Generic Constraints with keyof

You can use keyof to constrain the keys that can be used with a generic type.

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

let x = { a: 1, b: 2, c: 3, d: 4 };

console.log(getProperty(x, "a")); // 1
console.log(getProperty(x, "m")); // Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

Using Type Parameters in Generic Constraints

You can use type parameters in generic constraints to create more complex relationships between types.

function getInstance(c: new () => T): T {
    return new c();
}

class Animal {
    numLegs: number = 4;
}

class Bee extends Animal {
    keeper: BeeKeeper = new BeeKeeper();
}

class Lion extends Animal {
    keeper: ZooKeeper = new ZooKeeper();
}

createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!

Generic Functions with Multiple Type Parameters

You can define generic functions with multiple type parameters.

function merge(obj1: T, obj2: U): T & U {
    let result = {} as T & U;
    for (let id in obj1) {
        (result as any)[id] = (obj1 as any)[id];
    }
    for (let id in obj2) {
        if (!result.hasOwnProperty(id)) {
            (result as any)[id] = (obj2 as any)[id];
        }
    }
    return result;
}

let mergedObj = merge({ name: "John" }, { age: 30 });
console.log(mergedObj.name); // John
console.log(mergedObj.age); // 30

Generic Interfaces with Multiple Type Parameters

You can define generic interfaces with multiple type parameters.

interface Pair {
    first: T;
    second: U;
}

let pair: Pair = {
    first: "hello",
    second: 42
};

console.log(pair.first); // hello
console.log(pair.second); // 42

Conclusion

Generics in TypeScript provide a powerful way to create reusable and flexible components. By using generics, you can write code that works with a variety of types, improving the flexibility and reusability of your code. Whether you are working on functions, classes, or interfaces, mastering generics in TypeScript will help you write more robust and maintainable code.