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.
Table of Contents
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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.
1 2 3 4 5 6 7 | 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}
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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'.
1 2 3 4 5 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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"'. |