What is generics in TypeScript?
Generics in TypeScript are a tool for creating reusable components that can work with a variety of types rather than a single one. They allow you to write flexible, type-safe code without sacrificing type information or requiring `any`.
What are Generics?
Generics provide a way to pass types as parameters to other types, functions, classes, or interfaces. This concept is similar to how function parameters work for values; instead of operating on a specific type, a generic component can operate on a type variable, making it adaptable to different types while maintaining type safety.
Why Use Generics?
The primary reasons to use generics are:
- Reusability: Write a single component that works with different types, avoiding code duplication.
- Type Safety: Capture the type of arguments and ensure the return type matches, providing strong type checking at compile time.
- Flexibility: Adapt components to various data structures or scenarios without losing type information.
Example: The Identity Function
A common example to illustrate generics is the "identity function," which returns whatever is passed into it.
Without Generics (Problematic)
function identity(arg: any): any {
return arg;
}
While this identity function can accept any type, using any causes it to lose information about the type that was actually passed in. If you pass a number, TypeScript forgets that it was a number and only knows it's any.
With Generics (Solution)
function identity<T>(arg: T): T {
return arg;
}
// Usage:
let output1 = identity<string>("myString"); // type of output1 is string
let output2 = identity(123); // type of output2 is number (type inference)
Here, <T> introduces a type variable T. T acts as a placeholder for the actual type that will be provided when the identity function is called. When we call identity("myString"), T becomes string, and TypeScript knows that output1 is a string.
Common Use Cases for Generics
- Functions: For creating functions that operate on values of any type, like
identityor functions that map/filter arrays. - Interfaces: For defining data structures that can hold various types, e.g., a
Box<T>interface. - Classes: For building reusable data structures like
Stack<T>,Queue<T>, orLinkedList<T>. - Type Aliases: For creating flexible type definitions.
- React Components: To define props or state that are generic.
Type Parameters
The type variable, like T in <T>, is called a type parameter. You can use any valid identifier for type parameters, but T (for Type), U, V, K (for Key), E (for Element) are common conventions. Multiple type parameters can be used, e.g., function merge<T, U>(obj1: T, obj2: U): T & U.
Generic Constraints
Sometimes you want to operate on a type T but also need to ensure that T has certain properties. You can restrict the types that can be used with a generic by adding a constraint. This is done using the extends keyword.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now 'arg' is guaranteed to have a .length property
return arg;
}
// Usage:
loggingIdentity({ length: 10, value: "hello" }); // OK
// loggingIdentity(3); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
Key Takeaways
- Generics allow you to write components that work with any data type, providing flexibility.
- They maintain type safety, ensuring that type information isn't lost during operations.
- Type parameters (e.g.,
T) are placeholders for actual types. - Constraints (e.g.,
T extends SomeType) allow you to restrict the types that can be used with a generic, ensuring they have certain properties.