Advanced Typescript Generics tutorial

avatar

Borislav Hadzhiev

Tue Feb 23 20213 min read

banner

Photo by Mickey O'neil

The problem #

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

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');

In the above example we create an instance of the Attributes class passing in a generic of type EmployeeProps which has keys of name and salary, being respectively of string and number type.

We then use the get method on the instance and get the name attribute, however the type we get on the return value is either string or number, which 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. Name or salary are the only valid keys in the type T, which means we only should be getting 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:

get('name');

The corresponding type to the name key in the T type is 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.

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');

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 js - it looks up the value of the key K in the object T - which in our case is the value of the key name from the type EmployeeProps which is string.

Important points in the above snippet:

  • 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're saying look at the type T at the key K and return the value. In our 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".

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 keys of the EmployeeProps interface are strings of type name and salary, which 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.

Add me on LinkedIn

I'm a Web Developer with TypeScript, React.js, Node.js and AWS experience.

Let's connect on LinkedIn

Join my newsletter

I'll send you 1 email a week with links to all of the articles I've written that week

Buy Me A Coffee