TypeScript - Generic Classes
Hello, future coding superstars! Today, we're diving into the exciting world of TypeScript Generic Classes. Don't worry if you're new to programming; I'll guide you through this journey step-by-step, just like I've done for countless students over my years of teaching. So, grab your favorite beverage, get comfy, and let's embark on this adventure together!
Generic Classes
What are Generic Classes?
Imagine you're at an ice cream shop, but instead of choosing flavors, you're picking data types. That's the essence of generic classes! They allow us to create flexible, reusable components that can work with different data types without sacrificing type safety.
Let's start with a simple example:
class Box<T> {
private content: T;
constructor(value: T) {
this.content = value;
}
getValue(): T {
return this.content;
}
}
In this example, Box
is a generic class. The <T>
is like a placeholder for a type that we'll specify later. It's like telling the ice cream shop, "I'll decide on the flavor when I order!"
Let's break it down:
-
class Box<T>
: This declares a generic class namedBox
with a type parameterT
. -
private content: T
: We're saying thatcontent
will be of typeT
, whateverT
turns out to be. -
constructor(value: T)
: The constructor takes a value of typeT
. -
getValue(): T
: This method returns a value of typeT
.
Now, let's see how we can use this class:
let numberBox = new Box<number>(42);
console.log(numberBox.getValue()); // Output: 42
let stringBox = new Box<string>("Hello, TypeScript!");
console.log(stringBox.getValue()); // Output: Hello, TypeScript!
Isn't that cool? We've used the same Box
class to store both a number and a string. It's like having a magic box that can hold anything you put into it, but still remembers exactly what type of thing it's holding!
Multiple Type Parameters
Sometimes, one type parameter isn't enough. Let's create a more complex example with multiple type parameters:
class Pair<T, U> {
private first: T;
private second: U;
constructor(first: T, second: U) {
this.first = first;
this.second = second;
}
getFirst(): T {
return this.first;
}
getSecond(): U {
return this.second;
}
}
This Pair
class can hold two values of potentially different types. It's like having a duo ice cream cone where each scoop can be a different flavor!
Let's use our Pair
class:
let pair = new Pair<string, number>("Age", 30);
console.log(pair.getFirst()); // Output: Age
console.log(pair.getSecond()); // Output: 30
Generic Constraints
Sometimes, we want to limit what types can be used with our generic class. We can do this using constraints. It's like saying, "You can have any ice cream flavor, as long as it's not too spicy!"
interface Lengthwise {
length: number;
}
class LengthChecker<T extends Lengthwise> {
checkLength(obj: T): string {
return `The length is: ${obj.length}`;
}
}
In this example, T extends Lengthwise
means that T
must be a type that has a length
property. Let's use it:
let stringChecker = new LengthChecker<string>();
console.log(stringChecker.checkLength("Hello")); // Output: The length is: 5
let arrayChecker = new LengthChecker<number[]>();
console.log(arrayChecker.checkLength([1, 2, 3])); // Output: The length is: 3
// This would cause an error:
// let numberChecker = new LengthChecker<number>();
// Type 'number' does not satisfy the constraint 'Lengthwise'.
Implementing Generic Interface with Generic Classes
Now, let's take our skills to the next level by implementing a generic interface with a generic class. It's like creating a recipe (interface) for different types of ice cream (classes)!
First, let's define a generic interface:
interface Repository<T> {
getById(id: number): T;
save(item: T): void;
}
This Repository
interface defines a contract for classes that will handle data storage and retrieval. Now, let's implement this interface with a generic class:
class GenericRepository<T> implements Repository<T> {
private items: T[] = [];
getById(id: number): T {
return this.items[id];
}
save(item: T): void {
this.items.push(item);
}
}
Our GenericRepository
class implements the Repository
interface. It can work with any type T
. Let's use it:
interface User {
name: string;
age: number;
}
let userRepo = new GenericRepository<User>();
userRepo.save({ name: "Alice", age: 30 });
userRepo.save({ name: "Bob", age: 25 });
console.log(userRepo.getById(0)); // Output: { name: "Alice", age: 30 }
console.log(userRepo.getById(1)); // Output: { name: "Bob", age: 25 }
In this example, we've created a repository for User
objects. But the beauty of our generic implementation is that we could just as easily create a repository for any other type!
Methods Table
Here's a handy table summarizing the methods we've covered:
Method | Description | Example |
---|---|---|
constructor(value: T) |
Creates a new instance of a generic class | new Box<number>(42) |
getValue(): T |
Returns the value stored in a generic class | numberBox.getValue() |
getFirst(): T |
Returns the first value in a pair | pair.getFirst() |
getSecond(): U |
Returns the second value in a pair | pair.getSecond() |
checkLength(obj: T): string |
Checks the length of an object (with constraints) | stringChecker.checkLength("Hello") |
getById(id: number): T |
Retrieves an item from a repository by ID | userRepo.getById(0) |
save(item: T): void |
Saves an item to a repository | userRepo.save({ name: "Alice", age: 30 }) |
And there you have it, folks! We've journeyed through the land of TypeScript Generic Classes, from basic boxes to complex repositories. Remember, practice makes perfect, so don't be afraid to experiment with these concepts. Who knows? You might just create the next big thing in programming! Until next time, happy coding!
Credits: Image by storyset