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!

TypeScript - Generic Classes

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 named Box with a type parameter T.
  • private content: T: We're saying that content will be of type T, whatever T turns out to be.
  • constructor(value: T): The constructor takes a value of type T.
  • getValue(): T: This method returns a value of type T.

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