TypeScript Generic Classes

Generic classes in TypeScript allow you to define a class with types that are determined at the time of instantiation. This provides flexibility in working with different data types while maintaining type safety. By using generics in classes, you can create more reusable and scalable code.

 

What are Generic Classes?

A generic class is a class that works with multiple types, where the types are determined by the user when the class is instantiated. Generics allow a class to operate on different types without losing the benefits of TypeScript's static type checking.

 

Syntax of Generic Classes

The syntax for defining a generic class is similar to that of a generic function. You use a type parameter inside angle brackets (<T>) when declaring the class. This type parameter can be used for properties, methods, and the class constructor.

class MyClass<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}
  • <T>: This defines a type parameter T that can represent any type.
  • value: T: The class has a property value of type T.
  • getValue(): T: The method getValue returns a value of type T.

 

Example of a Generic Class

1. Generic Box Class

A simple example is a Box class that can store a value of any type:

class Box<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  setValue(value: T): void {
    this.value = value;
  }
}

const numberBox = new Box<number>(123);
console.log(numberBox.getValue()); // Output: 123

const stringBox = new Box<string>("Hello");
console.log(stringBox.getValue()); // Output: Hello
  • The Box class works with any type, such as number, string, or any other custom type.
  • You can instantiate the class with a specific type by passing it as a type argument (Box<number> or Box<string>).

2. Generic Class with Multiple Type Parameters

A class can accept multiple type parameters, making it more flexible for different use cases.

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;
  }
}

const pair = new Pair<number, string>(1, "One");
console.log(pair.getFirst());  // Output: 1
console.log(pair.getSecond()); // Output: One
  • In this example, the Pair class takes two type parameters: T and U.
  • You can instantiate the class with different types for T and U (e.g., Pair<number, string>).

 

Using Constraints in Generic Classes

Just like with generic functions, you can constrain the types in a generic class. This ensures that the type parameter adheres to a specific contract (e.g., it has certain properties or methods).

class Lengthy<T extends { length: number }> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  getLength(): number {
    return this.value.length;
  }
}

const stringInstance = new Lengthy("Hello");
console.log(stringInstance.getLength()); // Output: 5

const arrayInstance = new Lengthy([1, 2, 3]);
console.log(arrayInstance.getLength()); // Output: 3
  • Here, the type T is constrained to objects that have a length property.
  • This ensures that T can only be a type like string, array, or any object with a length property.

 

Generic Classes with Default Types

You can also provide default types for generics, making them optional when instantiating the class. If no type is provided, the default type is used.

class Container<T = string> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

const container1 = new Container(123);
console.log(container1.getValue());  // Output: 123

const container2 = new Container("Hello");
console.log(container2.getValue());  // Output: Hello

const container3 = new Container(true);
console.log(container3.getValue());  // Output: true
  • In this example, the default type for T is string. However, if a type is provided when creating the instance (e.g., number, boolean), it will be used instead.

 

Generic Classes with Methods

You can define methods in generic classes just like you would in a regular class, using the type parameter in the method signature.

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  getSize(): number {
    return this.items.length;
  }
}

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
console.log(stack.pop());  // Output: 2
console.log(stack.getSize());  // Output: 1
  • The Stack class works with any type (number, string, etc.).
  • The push method adds an item to the stack, and the pop method removes and returns the last item.

 

Summary

Generic classes in TypeScript offer powerful ways to create reusable and type-safe components. Some important takeaways include:

  • Reusability: Generic classes allow you to define classes that can work with multiple types.
  • Type Safety: You still benefit from TypeScript's static typing, ensuring that operations are performed correctly for the types involved.
  • Constraints: You can constrain the types that a generic class accepts, making your code more predictable.
  • Default Types: Default types simplify usage, especially for common use cases.

Generic classes are an essential feature in TypeScript for creating flexible, reusable, and type-safe code.