How to add constraints to the generic types?


TypeScript is an open-source programming language that provides type annotations to JavaScript. TypeScript has generic types that enable developers to write code that can work with different data types. Generics provide flexibility and reusability in code. However, in some cases, it is necessary to add constraints to generic types to restrict the types that can be used with them.

In this article, we will explain how to add constraints to generic types in TypeScript and we will also discuss some examples.

What are Generic Types?

Before we dive into constraints, let's first understand what generic types are. Generic types in TypeScript allow developers to define types that can work with various data types. This means that a function, class, or interface can take in any type of data, making the code more flexible and reusable.

For example, let's consider a simple function that takes in two arguments and returns the sum of those two arguments.

function add(a, b) {
   return a + b;
}

This function works fine for numbers, but what if we want to add two strings or two arrays? We would have to write separate functions for each data type, which is not efficient. Instead, we can use generic types to create a function that works with any data type.

function add<T>(a: T, b: T): T {
   return a + b;
}

Here, the function add has a generic type T, which can represent any data type. The function takes in two arguments of type T and returns a value of type T.

Adding Constraints to Generic Types

While generic types provide flexibility, in some cases, we may want to add constraints to restrict the types that can be used with them. Adding constraints can improve code quality and help catch errors at compile-time instead of runtime.

Constraints are added by using the keyword extends followed by the type or interface that we want to constrain the generic type to.

Syntax

function functionName<T extends ConstraintType>(arg1: Arg1Type, arg2: Arg2Type, ...): ReturnType {
   // function body
}
  • functionName − the name of the function that uses a generic type with a constraint

  • <T extends ConstraintType> − defines the generic type T and specifies the constraint using the extends keyword. ConstraintType can be any valid type in TypeScript, including interfaces, classes, and primitive types.

  • (arg1: Arg1Type, arg2: Arg2Type, ...) − the arguments passed to the function, along with their types.

  • : ReturnType − the return type of the function.

  • function body − the implementation of the function, which uses the generic type T constrained by the specified ConstraintType.

It is important to note that the constraint can only be applied to one generic type in the function. If multiple generic types are used, each one must have its own constraint. Additionally, it is possible to have multiple constraints on a single generic type by using an intersection type.

Examples

Example 1: Constraint on an Array

function getLength<T extends Array<any>>(arr: T): number {
   return arr.length;
}

In this example, we have added a constraint to the generic type T using the extends keyword. The constraint specifies that the type T must extend the Array<any>> type. This means that the function getLength can only be used with arrays.

We can take an example to check this out.

function getLength<T extends Array<any>>(arr: T): number {
   return arr.length;
}
const arr = [6, 7];
console.log(`The length of the array is: ${getLength(arr)}`);

On compiling, it will generate the following JavaScript code −

function getLength(arr) {
   return arr.length;
}
var arr = [6, 7];
console.log("The length of the array is: ".concat(getLength(arr)));

Output

The length of the array is: 2

Example 2: Constraint on an Object

In this example, we have added constraints to two generic types T and K using the extends keyword. The constraint specifies that the type T must extend the Person interface, and the type K must extend the keys of type T. This means that the function getProperty can only be used with objects of type Person and keys that exist in that interface.

interface Person {
   name: string;
   age: number;
}

function getProperty<T extends Person, K extends keyof T>(obj: T, key: K) {
   return obj[key];
}

const person: Person = {
   name: "John",
   age: 21,
};

console.log(`The name of the person is: ${getProperty(person, "name")}`);

On compiling, it will generate the following JavaScript code −

function getProperty(obj, key) {
   return obj[key];
}
var person = {
   name: "John",
   age: 21
};
console.log("The name of the person is: ".concat(getProperty(person, "name")));

Output

The name of the person is: John

Example 3: Constraint on a Class

In this example, we have added a constraint to the generic type T using the extends keyword. The constraint specifies that the type T must extend the Animal class. This means that the class Zoo can only be used with animal objects.

Class Animl {
}
class Animal {
   name: string;
   constructor(name: string) {
      this.name = name;
   }
}
class Zoo<T extends Animal> {
   animals: T[];
   constructor(animals: T[]) {
      this.animals = animals;
   }
}
const lion = new Animal('Lion');
const tiger = new Animal('Tiger');
const zoo = new Zoo<Animal>([lion, tiger]);
const zoo1 = new Zoo<Animl>([lion, tiger])  // gives error

Output

On compiling, it will generate the following error −

Type 'Animl' does not satisfy the constraint 'Animal'.
Property 'name' is missing in type 'Animl' but required in type 'Animal'.

Example 4: Constraint on a Function

In this example, we have added a constraint to the generic type T using the extends keyword. The constraint specifies that the type T must be any type that can be used with the Array type. This means that the function filter can only be used with arrays, and the second argument predicate must be a function that takes in an item of type T and returns a boolean value.

function filter<T>(array: T[], predicate: (item: T) => boolean): T[] {
   return array.filter(predicate);
}
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filter(numbers, (item) => item % 2 === 0);
console.log(`The numbers afer applying filter are: ${evenNumbers}`);

On compiling, it will generate the following JavaScript code −

function filter(array, predicate) {
   return array.filter(predicate);
}
var numbers = [1, 2, 3, 4, 5];
var evenNumbers = filter(numbers, function (item) { return item % 2 === 0; });
console.log("The numbers afer applying filter are: ".concat(evenNumbers));

Output

The numbers afer applying filter are: 2,4

Conclusion

In summary, adding constraints to generic types in TypeScript is a powerful feature that can help developers write more robust and flexible code. Constraints can be added to restrict the types that can be used with generic types, and this can improve code quality and catch errors at compile time. By using the extends keyword followed by the type or interface that we want to constrain the generic type to, developers can create more reliable and maintainable code.

Updated on: 22-Aug-2023

185 Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements