Generics in TypeScript are a way to create reusable and flexible components that can work with multiple types. They are useful because they let you create reusable code that can accept a variety of input types without sacrificing type safety.
When we write code, we often encounter situations where we need to perform the same operation on different types of data. To handle these cases without generics, we could either write separate functions for each data type, or use a less type-safe approach with the ‘any‘ type. The former leads to code duplication, while the latter sacrifices valuable type information.
Generics solve this problem by allowing us to create functions, classes, and interfaces that work with a placeholder type, which is later substituted with the actual data type when the code is invoked. This way, we can have a single implementation that works with different types, while maintaining type safety.
Let’s look at an example to illustrate generics. Suppose you want to create a function ‘identity‘ that simply returns its input value:
function identity<T>(arg: T): T {
return arg;
}
Here, ‘T‘ is the generic type parameter, representing the input and output type of the ‘identity‘ function. Now we can use this function with various types:
// The explicit way to provide the type argument
const numberIdentity = identity<number>(42);
const stringIdentity = identity<string>('Hello, TypeScript!');
// The TypeScript compiler can infer the type argument
const booleanIdentity = identity(true);
The generic function ‘identity‘ can be represented as:
∀T ⇒ identity : T → T
In this case, the generic type ‘T‘ is universally quantified, meaning it can be any type. When we call the ‘identity‘ function with a specific type, the generic type ‘T‘ is replaced with the actual type, maintaining complete type information.
Generics are also useful in classes and interfaces, providing generalized behavior that can be flexibly applied to different types.
Here’s an example of a generic ‘Stack‘ class:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
The ‘Stack‘ class can now be instantiated for specific types, such as:
const numberStack = new Stack<number>();
numberStack.push(42);
const poppedNumber = numberStack.pop(); // Typed as `number | undefined`
const stringStack = new Stack<string>();
stringStack.push('Hello, TypeScript!');
const poppedString = stringStack.pop(); // Typed as `string | undefined`
In summary, generics in TypeScript provide a means of writing reusable and type-safe code that can work with multiple data types. They enable the creation of flexible functions, classes, and interfaces, reducing code duplication and preserving valuable type information throughout our applications.