Generic Constraints in TypeScript

Using Generic Parameters we can create a method, class, or interface that accepts many types. But this may not be what we need. Sometimes we need to limit the types of values. This is where we use the generic constraints in TypeScript.

Generic Constraints

Generic constraints allow us to narrow down the type of a Generic Parameter. i.e. we limit the values that a generic parameter can accept.

Consider the following code. PrintMe is a generic method. It will accept any value. As you can see it will even take null & undefined.


function printMe<T>(arg: T): void {
  console.log(arg)
}


printMe(10)
printMe("Hello")
printMe(true)
printMe({name:"Topias Aamina"})
printMe(undefined)
printMe(null)

We want the function to accept only a string or a number. We do that by using the extends keyword.

T extends number|string in the following example, limits the T to either a number or string. Hence passing an object, boolean, null, or undefined results in a compiler error.

function printMe<T extends number|string>(arg: T): void {
  console.log(arg)
}


printMe(10)                   //ok
printMe("Hello")            //ok

printMe(true)                                   //error
printMe({name:"Topias Aamina"})     //error
printMe(undefined)                           //error
printMe(null)                                   //error

You can also extend from an empty object. Since String, Number, Boolean, etc are also subclasses of Object type, they are allowed. But null and undefined are not, hence they result in a compiler error.

function printMe<T extends {}>(arg: T): void {
  console.log(arg)
}


printMe(10)                        //ok
printMe("Hello")                   //ok
printMe(true)                      //ok     
printMe({name:"Topias Aamina"})    //ok

printMe(undefined)                 //error
printMe(null)                      //error

You can even extend from an Object (O in upper case), with a similar effect.

function printMe<T extends Object>(arg: T): void {
  console.log(arg)
}


printMe(10)                        //ok
printMe("Hello")                   //ok
printMe(true)                      //ok     
printMe({name:"Topias Aamina"})    //ok

printMe(undefined)                 //error
printMe(null)                      //error

If you wish to only accept objects i.e. non primitive types, then you can extend from an object (o in lower case). Note that the object is a Typescript-only type. You can’t assign to it any primitive type like bool, number, string, symbol, etc.

function printMe<T extends object>(arg: T): void {
  console.log(arg)
}


printMe({name:"Topias Aamina"})    //ok


printMe(10)                        //error
printMe("Hello")                   //error
printMe(true)                      //error     
printMe(undefined)                 //error
printMe(null)                      //error

Generic Constraints Examples

Constraining an object

Consider the following example, where we print the length of an object. The compiler throws the error because it cannot determine whether the arg has a length property.


function findLength<T>(arg: T): T {
   console.log(arg.length);     //Property 'length' does not exist on type 'T'.
   return arg;
}

We can extend the T from an object with a length property {length:number}.

function findLength<T extends {length:number}>(arg: T): T {
   // length property can now be called
   console.log(arg.length);
   return arg;
}

findLength(10)    //Error

findLength("Hello")    //ok
findLength({length:10, name:'Mobile'})    //ok
findLength([10,20])
  //ok

We can also extend from an interface. In the example, we create an interface ILength with property length and use that to constrain T

interface ILength {
  length:number
}

function findLength<T extends ILength>(arg: T): T {
   // length property can now be called
   console.log(arg.length);
   return arg;
}
 
findLength(10)    //Error
 
findLength("Hello")    //ok
findLength({length:10, name:'Mobile'})    //ok
findLength([10,20])    //ok

Type Parameter that is constrained by another Type Parameter

We can declare a type parameter that is constrained by another type parameter.

The getPropertyValue() function accepts an object (target) and a property name (key). It returns the value of the property. But this code results in an error Type 'K' cannot be used to index type 'T'.

function getPropertyValue<T, K> (target: T, key: K) {
  return target[key];          //Type 'K' cannot be used to index type 'T'.
}

Here we use bracket notation (target[key]) to access the value of a property. Typescript cannot guarantee that we accidentally use a property that does not exist.

Hence we use the Keyof type operator to create a new Type Parameter to extend the type T

function getPropertyValue<T, B extends keyof T> (target: T, key: B) {
  return target[key];
}

const person= {
  id: "0",
  name: "John Doe",
  age: 19
}

getPropertyValue(person, "id")   //ok

getPropertyValue(person, "SSN") //Argument of type '"SSN"' is not assignable to parameter of type '"id" | "name" | "age"'.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Scroll to Top