
Generic constraints
Sometimes, we might need to restrict the use of a generic class. For example, we can add a new feature to the generic queue. The new feature is going to validate the entities before they are added to the queue.
One possible solution would be to use the typeof operator to identify the type of the generic type parameter T within a generic class or function:
class User { public name!: string; public surname!: string; } class Car { public manufacturer!: string; public model!: string; } class Queue<T> { private _items: T[] = []; public push(item: T) { if (item instanceof User) { if ( item.name === "" || item.surname === "" ) { throw new Error("Invalid user"); } } if (item instanceof Car) { if ( item.manufacturer === "" || item.model === "" ) { throw new Error("Invalid car"); } } this._items.push(item); } public pop() { return this._items.shift(); } public size() { return this._items.length; } } const userQueue = new Queue<User>(); userQueue.push({ name: "", surname: "" }); // Runtime Error userQueue.push({ name: "Remo", surname: "" }); // Runtime Error userQueue.push({ name: "", surname: "Jansen" }); // Runtime Error const carQueue = new Queue<Car>(); carQueue.push({ manufacturer: "", model: "" }); // Runtime Error carQueue.push({ manufacturer: "BMW", model: "" }); // Runtime Error carQueue.push({ manufacturer: "", model: "M3" }); // Runtime Error
The problem is that we will have to modify our Queue class to add extra logic with each new kind of entity. We will not add the validation rules into the Queue class because a generic class should not know the type used as the generic type.
A better solution is to add a method named validate to the entities. The method will throw and exception if the entity is invalid:
class Queue<T> { private _items: T[] = []; public push(item: T) { item.validate(); // Error this._items.push(item); } public pop() { return this._items.shift(); } public size() { return this._items.length; } }
The preceding code snippet throws a compilation error because we can use the generic repository with any type, but not all types have a method named validate. Fortunately, this issue can easily be resolved by using a generic constraint. Constraints will restrict the types that we can use as the generic type parameter T. We are going to declare a constraint, so only the types that implement an interface named Validatable can be used with the generic method. Let's start by declaring the Validatable interface:
interface Validatable { validate(): void; }
Now, we can proceed to implement the interface. In this case, we must implement the validate method:
class User implements Validatable { public constructor( public name: string, public surname: string ) {} public validate() { if ( this.name === "" || this.surname === "" ) { throw new Error("Invalid user"); } } } class Car implements Validatable { public constructor( public manufacturer: string, public model: string ) {} public validate() { if ( this.manufacturer === "" || this.model === "" ) { throw new Error("Invalid car"); } } }
Now, let's declare a generic repository and add a type constraint so that only types that implement the Validatable interface are accepted:
class Queue<T extends Validatable> { private _items: T[] = []; public push(item: T) { item.validate(); this._items.push(item); } public pop() { return this._items.shift(); } public size() { return this._items.length; } }
At this point, we should be ready to see the new validation feature in action:
const userQueue = new Queue<User>(); userQueue.push(new User("", "")); // Error userQueue.push(new User("Remo", "")); // Error userQueue.push(new User("", "Jansen")); // Error const carQueue = new Queue<Car>(); carQueue.push(new Car("", "")); // Error carQueue.push(new Car("BMW", "")); // Error carQueue.push(new Car("", "M3")); // Error
If we attempt to use a class that doesn't implement the Validatable as the generic parameter T, we will get a compilation error.