Generics
Learn how to use generics to write reusable code that can work with different data types. We'll explore generic functions, classes, and interfaces.
TypeScript Generics for Beginners
What are Generics?
Generics in TypeScript allow you to write code that can work with a variety of data types without sacrificing type safety. Imagine you're creating a tool (like a box) that can hold different types of items. Instead of making separate boxes for each item type (e.g., a box for numbers, a box for strings), you can create a single, generic box that can adapt to the type of item you put inside. This "box" is like a generic function, class, or interface in TypeScript.
Essentially, generics provide a way to create reusable components where the type is a parameter.
Why Use Generics?
- Code Reusability: Write code once that works with multiple data types. Avoid writing redundant code for each specific type.
- Type Safety: Maintain type safety. The compiler still checks that you're using the correct types, preventing runtime errors.
- Flexibility: Your code becomes more flexible and adaptable to different situations.
- Improved Readability: Generics can make your code more understandable by making the type relationships explicit.
Generic Functions
Let's start with generic functions. A generic function uses type variables to represent the type(s) it will be working with. We usually use <T>
as the type variable, but you can use other names (e.g., <Type>
or <MyType>
) although T
is the convention.
Example: A Simple Identity Function
Here's a basic example of a generic identity function that returns the same value it receives:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello"); // myString is of type string
let myNumber: number = identity<number>(42); // myNumber is of type number
let myBoolean: boolean = identity<boolean>(true); // myBoolean is of type boolean
console.log(myString); // Output: hello
console.log(myNumber); // Output: 42
console.log(myBoolean); // Output: true
Explanation:
function identity<T>(arg: T): T
: This defines a function named `identity`. The<T>
introduces a type variable `T`. `arg: T` means the argument `arg` is of type `T`, and `): T` means the function returns a value of type `T`.identity<string>("hello")
: When calling the function, we specify the type `string` inside the angle brackets. This tells TypeScript that `T` is `string` in this specific call.- Type Inference: TypeScript can often infer the type argument automatically, so you can often omit the
<Type>
when calling the function:
let myString2 = identity("hello"); // TypeScript infers T is string
let myNumber2 = identity(42); // TypeScript infers T is number
Generic Interfaces
You can also use generics with interfaces. This is useful for creating reusable data structures.
Example: A Generic Interface for a Pair
interface Pair<T, U> {
first: T;
second: U;
}
let stringNumberPair: Pair<string, number> = {
first: "Name",
second: 123
};
console.log(stringNumberPair.first); // Output: Name
console.log(stringNumberPair.second); // Output: 123
Explanation:
interface Pair<T, U>
: Defines an interface called `Pair` with two type variables, `T` and `U`.first: T;
: The `first` property is of type `T`.second: U;
: The `second` property is of type `U`.Pair<string, number>
: When using the interface, we specify the types for `T` and `U` (string and number in this case).
Generic Classes
Similar to interfaces, you can create generic classes. This allows you to create classes that can operate on different types.
Example: A Generic Data Store
class DataStore<T> {
private data: T[] = [];
addItem(item: T): void {
this.data.push(item);
}
getItem(index: number): T | undefined {
return this.data[index];
}
}
let numberStore = new DataStore<number>();
numberStore.addItem(10);
numberStore.addItem(20);
console.log(numberStore.getItem(0)); // Output: 10
let stringStore = new DataStore<string>();
stringStore.addItem("apple");
stringStore.addItem("banana");
console.log(stringStore.getItem(1)); // Output: banana
Explanation:
class DataStore<T>
: Defines a class called `DataStore` with a type variable `T`.private data: T[] = [];
: The `data` property is an array of type `T`.addItem(item: T)
: The `addItem` method accepts an argument of type `T`.DataStore<number>
: When creating an instance of the class, we specify the type for `T` (number in the first case, string in the second).
Type Constraints
Sometimes, you need to restrict the types that can be used with your generics. This is done using type constraints.
Example: Constraining to a specific interface
interface Printable {
print(): void;
}
function printObject<T extends Printable>(obj: T): void {
obj.print();
}
class Document implements Printable {
content: string;
constructor(content: string) {
this.content = content;
}
print(): void {
console.log("Printing document: " + this.content);
}
}
const myDocument = new Document("This is my document.");
printObject(myDocument); // Valid
// This would cause a compile error because Number doesn't implement Printable:
// printObject(123);
Explanation:
<T extends Printable>
: This means that the type variable `T` must be a type that implements the `Printable` interface.- Only objects that have a
print()
method can be passed to theprintObject
function. This adds type safety.
Summary
Generics are a powerful feature in TypeScript that allow you to write reusable and type-safe code. They are essential for building robust and maintainable applications.
Key takeaways:
- Generics use type variables (usually
<T>
) to represent types. - You can use generics with functions, interfaces, and classes.
- TypeScript can often infer the type argument.
- Type constraints allow you to restrict the types that can be used with generics.