Generics serve as an essential tool in TypeScript, allowing developers to create flexible and reusable components that work with varying data types while maintaining strong type safety. In this article, we’ll break down what generics are, how they operate, and provide practical examples to illustrate their utility.
What Are Generics?
Generics are a way to create components that can handle different data types while still enforcing type constraints. You can think of generics as templates to write code that works independently of the data type. Instead of specifying a single type, you define a placeholder that can be replaced with any valid type during function or class instantiation.
Why Use Generics?
- Reusability: Generics help avoid code duplication by allowing you to write a function or class that can handle multiple types.
- Type Safety: Generics maintain strong type checking, catching issues at compile time rather than runtime.
- Flexible APIs: They allow the creation of flexible APIs that make libraries and frameworks more adaptable to various data types.
Basic Syntax
The syntax for defining generics in TypeScript involves using angle brackets <T> to define a type parameter. Here’s a basic example of a generic function:
function identity<T>(arg: T): T { return arg; } let result1 = identity<string>("Hello, Generics!"); // result1 is of type string let result2 = identity<number>(42); // result2 is of type number
In this example, identity is a generic function that takes a parameter arg of type T and returns a value of the same type. Note how we specify the type when calling the function.
Working with Multiple Types
Generics can also accept multiple type parameters. This is useful when you need to work with pairs of types:
function pair<U, V>(first: U, second: V): [U, V] { return [first, second]; } let result3 = pair<string, number>("Age", 30); // result3 is of type [string, number]
Here, the pair function takes two parameters of potentially different types and returns a tuple containing both values.
Generic Interfaces
Interfaces can also leverage generics, allowing you to define flexible data structures. Here’s an example of a generic interface:
interface Container<T> { value: T; } let numberContainer: Container<number> = { value: 123 }; let stringContainer: Container<string> = { value: "Generics" };
In this case, Container<T> can accommodate any type, maintaining type safety when you specify what type of value is being held.
Using Generics with Classes
You can utilize generics within classes. This follows a similar structure as functions and interfaces:
class Box<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } get(index: number): T | undefined { return this.items[index]; } } let stringBox = new Box<string>(); stringBox.add("Hello"); console.log(stringBox.get(0)); // Outputs: Hello let numberBox = new Box<number>(); numberBox.add(123); console.log(numberBox.get(0)); // Outputs: 123
In this example, the Box class can store items of any specific type decided during instantiation, either strings or numbers, promoting reusability.
Constraints with Generics
While generics offer flexibility, sometimes you need to impose constraints to ensure your generic types satisfy certain conditions. Use the extends keyword for constraining a type parameter:
function logLength<T extends { length: number }>(item: T): void { console.log(item.length); } logLength("Hello, Generics!"); // Works because string has a length property logLength([1, 2, 3]); // Works because arrays have a length property // logLength(42); // Error: Argument of type 'number' is not assignable to parameter of type '{ length: number; }'
This function accepts any type T that has a length property, thereby ensuring it can log the length of the passed argument.
Default Type Parameters
TypeScript allows you to declare default types for generics. This helps simplify your code when a common type can be inferred:
function wrap<T = string>(value: T): T { return value; } let wrappedValue = wrap(100); // The inferred type is number let wrappedString = wrap(); // The inferred type is string by default
In this example, wrap defaults to type string unless another type is provided.
Conclusion (Not Included)
Generics in TypeScript provide a powerful means of building type-safe and reusable components. By utilizing generics, you can enhance your code’s flexibility and maintainability while ensuring strong type checks help to catch errors early in the development process. With the concepts and examples in this blog, you should feel equipped to harness generics in your own TypeScript projects effectively.
