Advanced Typescript Generics tutorial

avatar
Borislav Hadzhiev

Last updated: Feb 29, 2024
3 min

banner

# The problem

Let's say we have a class named Attributes that manages attributes of different types - i.e. EmployeeAttributes, ClientAttributes, etc.

index.ts
type EmployeeProps = { name: string; salary: number; }; export class Attributes<T> { constructor(private data: T) {} get(key: string): string | number { return this.data[key]; } } const attrs = new Attributes<EmployeeProps>({ name: 'Tom', salary: 5000, }); // name is of type string or number because that's // what we have hardcoded as a return of the get method const name = attrs.get('name');

We created an instance of the Attributes class passing it a generic of type EmployeeProps.

The type has keys of name and salary of type string and number.

We used the get method on the instance to get the name attribute, however, the type we get on the return value is either string or number.

This means that we can only use operations on it common to strings and numbers before using a type guard to narrow down the type of the value.

We should be able to tell TypeScript that the name variable should be of type string, so we don't have to use type guards in this situation.

What we are trying to do is constrain the argument that the get method takes to be either name or salary.

The name and salary properties are the only valid keys in the type T.

We can only get values from the data object based on those keys.

Once we have constrained the arguments of the get method to be one of the keys of the type T, we should specify the return value of the get method to be the type corresponding to the passed-in key.

So if we for example call the get() method as follows.

index.ts
get('name');

The type of the name key in the T type is a string and that's the type the get() method should return.

# The solution - K extends keyof T generics

We can use a generic on the get method where we specify that generic type K can only be one of the keys of the generic type T which is passed to the class upon initialization.

If the argument of the get method is only constrained to be one of the keys of the T interface that is the signature of the data class property, then the get method should return T[K] - the type of the key K from the type T.

index.ts
type EmployeeProps = { name: string; salary: number; }; export class Attributes<T> { constructor(private data: T) {} get<K extends keyof T>(key: K): T[K] { return this.data[key]; } } const attrs = new Attributes<EmployeeProps>({ name: 'Tom', salary: 5000, }); // is now of type string const name = attrs.get('name'); // ERROR: Argument of type '"test"' is not assignable to parameter of type '"name" | "salary"'. const test = attrs.get('test');
The code for this article is available on GitHub

If you now hover over the name variable you should see that it is of type string because we've specified that the get method returns T[K], which is an object lookup just like in JavaScript.

It looks up the value of the key K in the object T. In our case, this is the value of the name key from the type EmployeeProps which is string.

Important points in the code sample:

  • The type of K can only ever be one of the keys of T - in our case, name or salary.

  • In other words, we can only call get with one of the keys of T - name or salary.

  • For the return value of get, we say "Look at the type T at the key K and return the corresponding type". In this case, look at the type EmployeeProps at the key name and return string.

# The how

We are only able to do this because in TYpeScript strings can be types. For example, the string "world" can be of type "world".

index.ts
type World = 'world'; function logHello(message: World) { console.log(`Hello ${message}`); } // ERROR: Argument of type '"all"' is not assignable to parameter of type '"world"'. logHello('all'); logHello('world');
The code for this article is available on GitHub

The keys of the EmployeeProps interface are strings of type name and salary.

This allows us to constrain the arguments the get method receives to the specific keys. Once we have done that, all we have to do is to annotate the return value of the get method as the lookup of the specific key K in the type EmployeeProps.

I've also written an article on how to use generics in arrow functions.

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.