TypeScript - Generic Constraints



In TypeScript, generic constraints allow you to specify limitations on the types that can be used with a generic type parameter. This adds an extra layer of type safety by ensuring the generic code only works with compatible data types.

Problem Examples

Before going deep into the generic constraints, let's understand the problem examples where you need to apply generic constraints.

Example

In the code below, we have created the merge() generic function, which takes the two objects as parameters, merges them using the spread operator, and returns the merged object.

After that, we invoke the merge() function by passing two objects as an argument, and it successfully prints the merged object.

// Generic function to merge two objects
function merge<T, U>(obj1: T, obj2: U) {
    return {...obj1, ...obj2};
}

// Invoke the function
const mergedObj = merge({name: 'Sam'}, {age: 30});
console.log(mergedObj); // Output: {name: 'Sam', age: 30}  

On compiling, it will generate the following JavaScript code.

// Generic function to merge two objects
function merge(obj1, obj2) {
    return Object.assign(Object.assign({}, obj1), obj2);
}
// Invoke the function
const mergedObj = merge({ name: 'Sam' }, { age: 30 });
console.log(mergedObj); // Output: {name: 'Sam', age: 30}  

Output

The output of the above example code is as follows –

{name: 'Sam', age: 30}

The merge() function has generic parameters. So, it can take the value of any data type as an argument including an object. What if you pass the boolean value as a second argument? Let's look at the example below.

Example

The below example code is very similar to the previous one. We have just changed the second argument of the merge() function to a Boolean value while calling it.

// Generic function to merge two objects
function merge<T, U>(obj1: T, obj2: U) {
    return {...obj1, ...obj2};
}

// Invoke the function
const mergedObj = merge({name: 'Sam'}, true);
console.log(mergedObj); // Output: {name: 'Sam'}  

On compiling, it will generate the following JavaScript code.

// Generic function to merge two objects
function merge(obj1, obj2) {
    return Object.assign(Object.assign({}, obj1), obj2);
}
// Invoke the function
const mergedObj = merge({ name: 'Sam' }, true);
console.log(mergedObj); // Output: {name: 'Sam'}  

Output

The output of the above example code is as follows –

{name: 'Sam'}

The above code prints the first object only in the output because the second argument was a Boolean value but not an object. To solve the problem found in the above example code, developers can use generic constraints.

How Generic Constraints Work in TypeScript?

Generic constraints allow us to limit the generic parameters to accept values of only particular types. i.e. you can narrow down the type of the generic parameters.

Syntax

You can follow the syntax below to use the generic constraints with generic parameters.

function merge<T extends object>(obj1: T) {
    // Code to execute
}
  • In the above syntax, 'T' is a generic type, 'extends' is a keyword, and 'object' is a data type.

  • Here, 'T' accepts only values having an 'object' data type.

Let's understand more about generic constraints via the example below. Now, if you try to compile the below code, you will get the compilation error as the generic parameter can accept only object argument but we are passing Boolean value.

// Generic function to merge two objects
function merge<T extends object, U extends object>(obj1: T, obj2: U) {
    return { ...obj1, ...obj2 };
}

// Invoke the function
const mergedObj = merge({ name: 'Sam' }, true);
console.log(mergedObj);

On compiling the above TypeScript code, the compiler shows the following error –

Argument of type 'boolean' is not assignable to parameter of type 'object'.

This way, we can limit the generic parameters to accept the values of a particular data type.

Example (Extending Generic Types with Interfaces)

Let's understand the code below with a step-by-step explanation.

  • We have defined the 'Person' interface which contains name, age, and email properties.

  • Next, we have defined the 'Employee' interface which contains 'empCode', and 'empDept' properties.

  • The merge() function contains two generic parameters T of type Person and U of type Employee.

  • In the merge() function, we merge both objects.

  • After that, we have defined two objects of type Person, and Employee, respectively.

  • Next, we invoke the merge() function by passing objects as an argument, and the code runs without any error.

// Define Person interface
interface Person {
    name: string;
    age: number;
    email: string;
}

// Define Employee interface
interface Employee {
    empCode: number;
    empDept: string;
}

// Generic function which takes Objects of the Person and Employee interfaces types
function merge<T extends Person, U extends Employee>(obj1: T, obj2: U) {
    return { ...obj1, ...obj2 };
}

// Create two objects
const person: Person = { name: 'John', age: 30, email: 'abc@gmail.com' };
const employee: Employee = { empCode: 1001, empDept: 'IT' };

// Invoke the function
const mergedObj = merge(person, employee);
console.log(mergedObj);

On compiling, it will generate the following JavaScript code.

// Generic function which takes Objects of the Person and Employee interfaces types
function merge(obj1, obj2) {
    return Object.assign(Object.assign({}, obj1), obj2);
}
// Create two objects
const person = { name: 'John', age: 30, email: 'abc@gmail.com' };
const employee = { empCode: 1001, empDept: 'IT' };
// Invoke the function
const mergedObj = merge(person, employee);
console.log(mergedObj);

Output

The output of the above example code is as follows –

{
  name: 'John',
  age: 30,
  email: 'abc@gmail.com',
  empCode: 1001,
  empDept: 'IT'
}

Using Type Parameters in Generic Constraints

TypeScript also allows you to define a type parameter, which is constrained by another parameter of the same function.

Let's understand it via the example below.

Example

In the code below, type 'U' extends the keys of the object received in the first parameter. So, it will accept the keys of the obj object as an argument to avoid errors in the function body.

Next, we invoke the getValue() function by passing the 'obj' object as an argument. It prints the key value in the output.

// Parameter U ensures that the key is a valid key of the object T.
function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
    return obj[key];
}

// Define an object
const obj = {
    name: 'Sam',
    age: 34
};

// Get the value of the key 'name'
const name1 = getValue(obj, 'name');
console.log(name1); // Sam

On compiling, it will generate the following JavaScript code.

// Parameter U ensures that the key is a valid key of the object T.
function getValue(obj, key) {
    return obj[key];
}
// Define an object
const obj = {
    name: 'Sam',
    age: 34
};
// Get the value of the key 'name'
const name1 = getValue(obj, 'name');
console.log(name1); // Sam

Output

The output of the above example code is as follows –

Sam

We understood that generic constraints are useful to accept the values of the specific data types as a parameter, instead of taking values of all data types.

Advertisements