Generic classes enable us to write code that can work with different data types while retaining strong typing. In this article, we will dive deep into the concept of generic classes, exploring their syntax, benefits, and practical applications. By the end of this journey, you'll be equipped with the knowledge to leverage generic classes in TypeScript to build more reusable and robust software.
A generic class is a class that can operate on multiple data types, using a type parameter (often denoted by a single uppercase letter) to represent the generic type. The type parameter acts as a placeholder for the actual data type that will be used when creating instances of the class.
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const numberBox = new Box<number>(42);
const stringBox = new Box<string>("Hello, TypeScript!");
console.log(numberBox.getValue()); // Output: 42
console.log(stringBox.getValue()); // Output: "Hello, TypeScript!"
In this example, we define a generic class Box<T>
, which can store and retrieve values of different data types. The type parameter T
acts as a placeholder for the actual type, which is specified when creating instances of the class.
One of the main advantages of generic classes is reusability. By defining a class with a type parameter, we create a blueprint that can be used for various data types without duplicating code.
class Pair<T, U> {
constructor(public first: T, public second: U) {}
}
const stringNumberPair = new Pair<string, number>("John", 30);
const booleanArrayPair = new Pair<boolean, number[]>(true, [1, 2, 3]);
console.log(stringNumberPair.first, stringNumberPair.second); // Output: "John" 30
console.log(booleanArrayPair.first, booleanArrayPair.second); // Output: true [1, 2, 3]
In this example, the Pair
class is generic and can store pairs of different data types, such as strings and numbers or booleans and arrays.
Generic classes ensure type safety by preserving the relationship between the properties and methods of the class and the type parameter.
class Pair<T> {
constructor(public first: T, public second: T) {}
}
const numberPair = new Pair<number>(1, 2);
const stringPair = new Pair<string>("Hello", "World");
numberPair.first = "Hello"; // Error: Type '"Hello"' is not assignable to type 'number'
stringPair.second = 42; // Error: Type '42' is not assignable to type 'string'
In this example, the Pair
class guarantees that both properties first
and second
will be of the same type T
, preventing unintended type mismatches.
Just like with generic functions, we can impose constraints on generic classes to ensure they work only with specific data types.
interface Printable {
print(): void;
}
class PrintBox<T extends Printable> {
constructor(private value: T) {}
printValue(): void {
this.value.print();
}
}
class Person implements Printable {
constructor(private name: string) {}
print(): void {
console.log(`Name: ${this.name}`);
}
}
const john = new Person("John Doe");
const box = new PrintBox<Person>(john);
box.printValue(); // Output: "Name: John Doe"
In this example, the PrintBox
class is constrained to work only with types that implement the Printable
interface. This ensures that any instance of PrintBox
can call the print()
method on its value.