Article image Generics in TypeScript

18. Generics in TypeScript

Page 18 | Listen in audio

Generics in TypeScript are a powerful feature that allows developers to create reusable and flexible components. They enable you to define functions, interfaces, and classes that work with a variety of data types while maintaining strong type safety. By using generics, you can create code that is not only more abstract and reusable but also type-safe and expressive.

At its core, a generic is a way to define a component that can work with any data type. This is achieved by using a type parameter, which acts as a placeholder for the actual data type that will be used when the component is utilized. The type parameter is typically represented by the letter T, but it can be any valid identifier.

Basic Usage of Generics

To illustrate the basic usage of generics, let's start with a simple example. Suppose we want to create a function that returns the first element of an array. Without generics, we would need to define a separate function for each data type:

function getFirstNumber(numbers: number[]): number {
    return numbers[0];
}

function getFirstString(strings: string[]): string {
    return strings[0];
}

With generics, we can create a single function that works with any data type:

function getFirstElement<T>(elements: T[]): T {
    return elements[0];
}

In this example, T is a type parameter that represents the type of the elements in the array. When we call the getFirstElement function, TypeScript infers the type of T based on the argument we pass:

const firstNumber = getFirstElement([1, 2, 3]); // T is inferred as number
const firstString = getFirstElement(['a', 'b', 'c']); // T is inferred as string

Generic Interfaces

Generics can also be used with interfaces. This allows you to define interfaces that work with any data type. For example, consider a simple interface for a key-value pair:

interface KeyValuePair<K, V> {
    key: K;
    value: V;
}

Here, K and V are type parameters representing the types of the key and value, respectively. You can then use this generic interface to create key-value pairs for any data types:

const numberToString: KeyValuePair<number, string> = { key: 1, value: 'one' };
const stringToNumber: KeyValuePair<string, number> = { key: 'one', value: 1 };

Generic Classes

Classes can also be generic, allowing you to create data structures that work with any data type. Consider a simple stack implementation:

class Stack<T> {
    private items: T[] = [];

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

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }

    isEmpty(): boolean {
        return this.items.length === 0;
    }
}

In this example, the Stack class is generic and can store items of any type. You can create stacks for different data types and use them as needed:

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // Outputs: 2

const stringStack = new Stack<string>();
stringStack.push('a');
stringStack.push('b');
console.log(stringStack.pop()); // Outputs: 'b'

Generic Constraints

Sometimes, you may want to restrict the types that can be used with a generic component. This is where generic constraints come into play. You can use constraints to specify that a type parameter must extend a certain type or interface.

For example, suppose you want to create a function that logs the length of an array-like object. You can use a generic constraint to ensure that the type parameter extends the { length: number } interface:

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

logLength([1, 2, 3]); // Valid, as arrays have a length property
logLength('hello');   // Valid, as strings have a length property
// logLength(123);    // Error, as numbers do not have a length property

Default Type Parameters

TypeScript also allows you to specify default values for type parameters. This can be useful when you want to provide a default type but still allow users to override it if needed.

Consider a function that creates an array of a given length, filled with a specified value. You can provide a default type for the value using default type parameters:

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

const numbers = createArray(3, 0); // T is inferred as number
const strings = createArray<string>(3, 'a'); // T is explicitly set to string

Using Multiple Type Parameters

Generics in TypeScript are not limited to a single type parameter. You can use multiple type parameters to create more complex and flexible components. For example, consider a function that swaps the values of two variables:

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

const swapped = swap(1, 'a'); // [string, number]
console.log(swapped); // Outputs: ['a', 1]

In this example, the swap function uses two type parameters, T and U, allowing it to swap values of any two types.

Practical Applications of Generics

Generics play a crucial role in creating reusable and type-safe code in TypeScript. They are extensively used in libraries and frameworks, such as Angular and React, to provide flexible and robust APIs. By understanding and utilizing generics, you can enhance the flexibility and maintainability of your code.

Consider a scenario where you need to implement a data fetching service that can handle different types of data. By using generics, you can create a service that abstracts the data type while maintaining type safety:

class DataService<T> {
    fetchData(url: string): Promise<T> {
        return fetch(url).then(response => response.json());
    }
}

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

const userService = new DataService<User>();
userService.fetchData('/api/users/1').then(user => {
    console.log(user.name);
});

In this example, the DataService class is generic and can be used to fetch data of any type. The type parameter T is used to ensure that the data returned by the fetchData method is of the expected type.

Conclusion

Generics in TypeScript provide a powerful mechanism for creating flexible and reusable components. By using type parameters, developers can create functions, interfaces, and classes that work with a variety of data types while maintaining strong type safety. Whether you're building utility functions, data structures, or complex services, generics can help you write more abstract, reusable, and type-safe code. As you continue to explore TypeScript, you'll discover that generics are an indispensable tool in your development toolkit.

Now answer the exercise about the content:

What is the primary benefit of using generics in TypeScript as mentioned in the text?

You are right! Congratulations, now go to the next page

You missed! Try again.

Article image Generic Constraints

Next page of the Free Ebook:

19Generic Constraints

8 minutes

Earn your Certificate for this Course for Free! by downloading the Cursa app and reading the ebook there. Available on Google Play or App Store!

Get it on Google Play Get it on App Store

+ 6.5 million
students

Free and Valid
Certificate with QR Code

48 thousand free
exercises

4.8/5 rating in
app stores

Free courses in
video, audio and text