DEV Community

Kento Honda
Kento Honda

Posted on

Constrained Generic Types in TypeScript

Intro

It has been a while since I wrote an article regarding the basic concepts of TypeScript because I have started a new job working as a Front-End Developer for a startup company.

Then finally, I could read through the chapter of learningTypeScript talking about generic types, which are one of the most important features of TypeScript. To get a deeper understanding of generic types, I repeatedly read sections written about the fundamentals of generic types.

Also, I could notice and learn informative and useful tips on generic types, especially constrained generic types.

Having a correct understanding of generic types would allow us to level up as TypeScript learners and expand the usage of TypeScript in more complex type definitions such as in professional environments.

So this time, I only focus on covering constrained generic types but reviewing all aspects of generic types in TypeScript is pretty worth it for improving our TypeScript skills.

Now let's dive into the topic!

To begin with, what are generic types all about?

To put generic types in a word, that is "type parameter".

So what does type parameter stand for? Type parameter

// In general, we can set type parameter as T
function printValue<T>(value:T) {
  console.log(typeof value)
  return value
}

printValue(123) // Output of console.log: number
printValue('Sample') // Output of console.log: string
printValue(false) // Output of console.log: boolean

Enter fullscreen mode Exit fullscreen mode

As you can see in the code snippet above, generic types are useful when it is uncertain that we could not identify the type value of parameters such as those for functions.

Thanks to generic types, we could pass different type values but still, the type safety is secured. However, the basic usage of generic types like the code sample above is not perfect to handle more complex type definitions.

Accepting any type value as a type parameter includes the possibility of getting type errors based on actual arguments you pass and the pitfall that type checking implemented by TypeScript does not work properly.

Then, constrained generic types play their roles in complementing this shortcoming of generic types. Let's see how constrained generic types work in the next section.

Constrained generic types

TypeScript gives us the option to handle the cases such as some functions only working with a specific, limited set of types.

For example, TypeScript complains about the type error because of passing a non-specified type value as the type parameter in the code snippet below.

function printArrayLength<T>(array: T){
    return array.length // error: Property 'length' does not exist on type 'T'.
}
Enter fullscreen mode Exit fullscreen mode

So in this case, the printArrayLength function expects to receive any kind of array and then, return the length of the array passed as the parameter.

But since generic type T accepts any type values which are not limited to the array, a type error appears (some arguments whose types are number, boolean, and so on don't have length property in it.

So, in order to fix the problem, we can limit the assignable values to generic type by using extend keyword together.

function printArrayLength<T extends U[],U>(array: T){
    return array.length
}
console.log(printArrayLength(['a','b','c'])) // OK, Output: 3
console.log(printArrayLength([1,2,3,4,5])) // OK, Output: 5
Enter fullscreen mode Exit fullscreen mode

As you can see in the code sample above, we don't have error anymore. Thanks to the power of constrained generic type, we could successfully set the limitation for generic type T (Now we cannot pass any type values except for array).

In this case, I use two generic types as T and U. We can define generic types whatever we want, but it is common that we should avoid using more than three generic types at the same time.

T extends U[] means that I specify the type value of T as U[], and now it is possible to pass any type of array (such as an array with string, number, boolean, and so on) because the generic type U accept any possible types.

Another useful case of Constrained generic types

Constrained generic types are helpful to prevent from having the situation that TypeScript didn't implement type checking properly.

Let me get this straight now. In the code below, TypeScript doesn't show any errors, but still, arguments passed to the function and its return value are not exactly the same as we expect.

function combineTwoObj<T,U>(firstObj:T,secondObj:U) {
    return {
        ...firstObj,
        ...secondObj
    }
}
console.log(combineTwoObj({name:'Tom',age:22},10)) 
// Output: { "name": "Tom", "age": 22} But this output is not an expected one. 
Enter fullscreen mode Exit fullscreen mode

In this scenario, what we would like to implement with the combineTwoObj function is to combine two objects passed as parameters and return a new object containing all key-value pairs of them.

However, it also works when not passing exactly two objects (one of them is just a number in this case). So this means limiting the type of generic type is needed to get the ideal result of combineTwoObj function.

What we can do to achieve that goal is to add extends keyword after T and U.

function combineTwoObj<T extends object,U extends object>(firstObj:T,secondObj:U) {
    return {
        ...firstObj,
        ...secondObj
    }
}
console.log(combineTwoObj({name:'Tom',age:22},10))  // Argument of type 'number' is not assignable to parameter of type 'object'.
console.log(combineTwoObj({name:'Tom',age:22},{hobby: 'running', isAdult: true})) // Ok, Output: { "name": "Tom","age": 22,"hobby": "running","isAdult": true }
Enter fullscreen mode Exit fullscreen mode

In addition, utilizing keyof operator with generic types is also useful when it is necessary to get the value of object with the type-safe.

function printObjValue<T,U extends keyof T>(obj:T,key:U) {
    return obj[key]
}
console.log(printObjValue({name:'Tom',age:22},"name")) // Ok, Output: "Tom"
console.log(printObjValue({name:'Tom',age:22},"height")) // Error: Argument of type '"height"' is not assignable to parameter of type '"name" | "age"'.
Enter fullscreen mode Exit fullscreen mode

In this code snippet above, generic type U is specified as the type keyof T. Because of this type of specification, we can safely apply key value to the object.

If you are not familiar with keyof operator in TypeScript, you can refer to this link to the official document of TypeScript demonstrating about keyof operator.

Keyof Type Operator

Conclusion

Throughout this article, I could realize that taking advantage of generic constraints significantly improves the quality of type safety in TypeScript.

Understanding generic types in TypeScript seems to be one of the challenging parts for TypeScript learners looking to advance to the next level. I am also still on the process of mastering generic types in TypeScript, so let's keep learning TypeScript together to get a comprehensive understanding of it!

References

Top comments (0)