Typescript generics - An appendix to the Typescript five minute tutorial

Leonie

When I started using TypeScript, some years ago, the official website was of course the first place I looked for information. The 5 minute tutorial provides a quick introduction about how to set up TypeScript in your project and how to use it. But once I got into TypeScript, I soon ran into questions about how to apply TypeScript in dynamic code. Not having a background in typed languages, the concept of generics was unknown to me. It would have helped me a lot if there had been another 5 minute tutorial about how to apply TypeScript in a dynamic way, so I thought I’d write one myself.

Reusable code

As a beginning TypeScript-developer, whenever you want to make a method more flexible and dynamic, you may be tempted to use any or unknown to suppress the inevitable TypeScript-error. You know there must be a better way to handle such code. This is where generics come in.

Generics enable you to create the dynamic, reusable code needed to make an api-service handle different types of response-objects, or to make your utility-methods indifferent to the type of value passed to them. According to the official TypeScript website, generics are about

"…​being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types."

TypeScript gives the following example. The <T> indicates to TypeScript this is a generic method. TypeScript will inject the typing provided by the calling method into the function typed as generic:

function identity<T>(arg: T): T {
    return arg;
}

const output = identity<string>("myString");

In this example, the caller doesn’t actually need to provide a typing as TypeScript is capable itself of inferring this primitive type, but we’ll declare one to give you the full picture.

Running over arrays

Let’s take a more common usecase: we’ll create a method that runs over an array.

In non-TypeScript code, our method may look like this:

function findInArray(arr, value) {
    console.log(`Looping  ${arr.length} times over the array`);
    return arr.filter(item => item === value);
}

Using generics, we could convert the example above in the following way:

function findInArray<T>(arr:T, value:T): T {
    console.log(`Looping  ${arr.length}  times over the array`);
    return arr.filter(item => item === value);
}

Unfortunately, this example is not yet quite what we want. First of all, you’ll notice an error:

TS2339: Property  ‘length’ does not exist on type T.

A similar error occurs for the calling of .filter(). Although we get an error, this example also illustrates the benefit of using generics functions: T may be generic, but TypeScript makes no assumptions about the parameters provided to a generic function.

In this case, we can fix this by indicating T is an array:

function findInArray<T>(arr:T[], value:T): T[] {
    console.log(`Looping  ${arr.length}  times over the array`);
    return arr.filter(item => item === value);
}

Now we have a working method: the array consists of type T, that implies the value we look for is also of type T.

Objects

So now we have a simple example of how we can iterate over an array while the method doesn’t need to know what is in the array. How about objects?

In vanilla javascript we could write a simple method like the following:

function findInObject(obj, key)  {
    return obj[key];
}

To convert this method into TypeScript, we could do the following:

function findInObject<T, K>(obj: T, key:K) {
   return obj[key];
}

You may notice this method, which requires two parameters, now also declares K in its’ generic type declaration. The actual naming is irrelevant to TypeScript. We use K here as a reference to the word ‘key’ but any letter or word will do.

Our naive example however results in a TS2536 error:

Type ‘K’ can not be used to index type ‘T’

TypeScript does not recognize that the given key exists in T. You need to express this in your declaration. This can be done by adding the following constraint:

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

We added an additional type check to determine if K is indeed a key of T. We improved the method further by declaring the return value, so we pass this typing back to the calling method.

Interfaces

From the above we’ve learned that in a generic function we can apply the builtin interface keyof. TypeScript has a number of these builtin interfaces, but we can also create our own.

Suppose we want to check the length of results in a response object. Our codebase deals with a range of different api-calls, so we may not always depend on the same type of result. We do know that our backend adds the property ‘count’ to each result, so we can count on that.

How do we tell our method to accept only objects with a property named count? We will declare an interface with the name countResults:

interface CountResults {
    count: number;
}

function checkResults<T extends CountResults>(arg: T): T | void {
   if (arg.count > 0) {
       return arg;
   }
}

Summary

Generics are the key to making your TypeScript code more dynamic. Generics can be applied not only to functions but also to classes. With generic classes, you’ll be able to create factory classes and other cool stuff. It would take a tutorial somewhat longer than five minutes to explain this here.

For further reading I recommend the official TypeScript documentation and this excellent article by Ross Bulat on Medium.com:

Put your skeletons in the closet - Build your next project using Parcel Out of your comfort zone — Let's make a game: Sprite animation with canvas