Progressive Full Stack Application Development with Live Projects

TypeScript

Generics in Typescript

Generics in TypeScript are a powerful feature that allow you to create reusable and flexible components by defining types that can be specified later.

Generics enable us to write more flexible and reusable code while maintaining type safety. Whether we’re working with functions, interfaces, or classes, generics help ensure that the code can handle various types in a predictable manner.

Generics appear in TypeScript code inside angle brackets, in the format <T>, where T represents a passed-in type. <T> can be read as a generic of type T.

Basic Generics

Here’s a simple example of a generic function:

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

let numberIdentity = identity<number>(42);  // type of numberIdentity is number
let stringIdentity = identity<string>("Hello, TypeScript!");  // type of stringIdentity is string

In this example, T is a placeholder for a type that will be provided when the function is called. The identity function returns the value it receives, and the type of that value is preserved.

Generics with Classes

You can also use generics with classes:

class Box<T> {
    private value: T;

    constructor(value: T) {
        this.value = value;
    }

    getValue(): T {
        return this.value;
    }
}

let numberBox = new Box<number>(123);
console.log(numberBox.getValue());  // Output: 123

let stringBox = new Box<string>("Generic Type");
console.log(stringBox.getValue());  // Output: Generic Type

In this example, Box is a generic class that can hold a value of any type, and the type of the value is specified when creating an instance of the class.

Generics with Interfaces

Interfaces can also be generic:

interface Pair<T, U> {
    first: T;
    second: U;
}

let numberStringPair: Pair<number, string> = {
    first: 1,
    second: "one"
};

let booleanArrayPair: Pair<boolean, number[]> = {
    first: true,
    second: [1, 2, 3]
};

Here, the Pair interface takes two type parameters, T and U, and can be used to create objects that pair two values of different types.

Constraints in Generics

You can impose constraints on generics to restrict the types that can be used:

interface Lengthy {
    length: number;
}

function logLength<T extends Lengthy>(item: T): void {
    console.log(item.length);
}

logLength("Hello, world!");  // Output: 13
logLength([1, 2, 3]);        // Output: 3

// logLength(123);  // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthy'.

In this example, the logLength function only accepts arguments that have a length property, which is enforced by the Lengthy interface.

Using Multiple Generics

You can use multiple generic parameters for more complex scenarios:

function combine<T, U>(a: T, b: U): [T, U] {
    return [a, b];
}

let combined = combine(1, "one");  // type of combined is [number, string]
console.log(combined);  // Output: [1, "one"]

In this function, combine takes two arguments of different types and returns a tuple containing both arguments.

Generic Utility Types

TypeScript also provides several built-in generic utility types:

  • Partial<T>: Makes all properties of T optional.
  • Required<T>: Makes all properties of T required.
  • Readonly<T>: Makes all properties of T read-only.
  • Record<K, T>: Constructs an object type with properties K of type T.

Here’s an example using Partial and Readonly:

interface User {
    name: string;
    age: number;
}

const user: Partial<User> = {
    name: "Alice"
    // age is optional
};

const readonlyUser: Readonly<User> = {
    name: "Bob",
    age: 30
};

// readonlyUser.age = 31;  // Error: Cannot assign to 'age' because it is a read-only property.

Generics enhance the flexibility and reusability of your code while keeping it type-safe. By defining functions, classes, and interfaces with generics, you can write more robust and adaptable TypeScript code.

Leave a Reply