TypeScript Utility types with Generics

TypeScript Utility types with Generics

Β·

3 min read

This one will be a little bit more advanced, as we'll be looking at improving our combined utility type we made the other day.

The code so far looks like this:

interface User {
  id?: number;
  firstname: string;
  lastname: string;
  age?: number;
}

type LoggedUser = Required<Pick<User, 'id'>> & Omit<User, 'id'>;

const u1:LoggedUser = {
  id: 1,
  firstname: 'Chris',
  lastname: 'Bongers'
}

The LoggedUser type is a modified version of the User interface, requiring specific fields.

In our case, we make the id field required.

However, this kind of action of requiring fields might become a feature we would like to re-use throughout our application.

And by looking at generics types, it's exactly what we can use to make this happen.

Making a generic require fields utility type

We would love to have a RequireFields type. This type could then be used to define a list of required fields for a specific type.

The great part about types is that we can define information in their generics section like so:

type RequireFields<Type>

The Type will now be available to work with inside the function.

Let's take a step back and see what details we need.

type LoggedUser = Required<Pick<User, 'id'>> & Omit<User, 'id'>;

By looking at the above, we see that we need the User type and the field we want to require, the id.

When we looked at generics types, I briefly mentioned there is not a limit to one type so that we can pass multiple types like this:

type RequireFields<Type1, Type2>

The first one in our case will be User, which we can define as T. However, the second one is a bit different since it can contain one or multiple keys from this T (User).

Luckily for us, TypeScript has a feature that does just that.

The function looks like this:

K extends keyof T

Here we define K as our second type, and K should act as an extended key object of the T.

Let's quickly look at what this could return to see what we are working with.

Using type of to get the keys

As you can see in the image above, the keys for this interface are: "id" | "firstname" | "lastname" | "age".

By using extends keyof Type, we make sure we can only pass keys that are part of the object.

Looping back to our RequireFields type, we can set the generic types to be the following:

type RequireFields<T, K extends keyof T>

In this case, the T will be our type, and the K will be the keys of this type we want to use.

Note: Type keys can be one or multiple delimited by an |.

Then we can modify what we had before to work with these two generic types.

Before:

type LoggedUser = Required<Pick<User, 'id'>> & Omit<User, 'id'>;

After:

type RequireFields<T, K extends keyof T> = Required<Pick<T, K>> & Omit<T, K>;

We can simply call this RequireFields type and pass the type and keys we want to require.

const u2:RequireFields<User, 'id' | 'age'> = {
  id: 2,
  age: 32,
  firstname: 'Chris',
  lastname: 'Bongers'
}

Remember when I said the extends keyof will check for the right keys? Let's try and modify the age key to a key that doesn't exist.

Passing wrong keys

In this image, you can quickly see TypeScript will warn us that this email field does not exist on the User type.

Conclusion

This is quite a complex concept to grasp at first, and I urge you to try it out yourself.

By playing around and following the steps, you should understand what this code does in detail.

These generic types combined with the utility types make TypeScript super exciting and versatile.

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Did you find this article valuable?

Support Daily Dev Tips by becoming a sponsor. Any amount is appreciated!