Transform Existing Types Using Mapped Types in TypeScript

Marius Schulz
InstructorMarius Schulz

Share this video with your friends

Send Tweet
Published 6 years ago
Updated 4 years ago

Mapped types are a powerful and unique feature of TypeScript's type system. They allow you to create a new type by transforming all properties of an existing type according to a given transformation function. In this lesson, we'll cover mapped types like Readonly<T> or Partial<T> that ship with the TypeScript compiler, and we'll also explore how to create our own type transformations.

For this lesson, I've created a simple point type of two properties, x and y. Let's go ahead and create a local variable of that type, called origin, with both of its properties set to zero. I've used the "const" keyword to declare the variable, but that doesn't make the properties immutable.

If I want to go ahead and change the property values I can and I don't get any type errors. We could make our origin totally immutable by calling the Object.freeze method. I will go ahead and delete the type annotation here. I'm going to call "Object.freeze." I'm also going to pass in the type argument explicitly.

If I hover over the property assignments here, I get a type error saying that we cannot assign to x, because it is a read-only property. The same goes for y. How come that x and y are suddenly read-only? Up here in our point interface, we have not added the read-only modifier.

Let's jump to the definition of the Object.freeze method and see how it's defined. Object.freeze accepts a single parameter of the generic type t. Then it returns an object that is typed as read-only t. What's read-only? Let's jump to that definition.

Read only t is the first mapped type that we're going to look at in this lesson. It is defined within the lib.d.ts definition file that ships with the typescript compiler. Read-only t makes all properties of a given type read-only.

In general, a mapped type defines a transformation that is applied to every single property. You can read the syntax as follows. For each property p that the type t defines, we want to add the read-only keyword and we want to keep the original type.

Within this formula we're using the key off operator that we looked at in the last lesson and a lookup type. If we go back to our code and hover over the origin variable, we can see the typescript has inferred that is of type read-only point.

Let's check out what that looks like by defining a type alias read-only point by setting that equal to read-only point. As we can see, the type read-only point defines two properties x and y, both of which are marked read-only.

This is why we cannot assign to x and y up here. Now let's dissect the syntax of map types step-by-step, so it starts to make a little more sense. To do that, I'm going to copy the definition of the read-only t type and we're going to resolve it step-by-step here.

First, we're going to replace every occurrence of the generic type t by a point, because we've specified the type argument point for the type parameter t. Next we're going to resolve key off point. We're going to collect all the keys that point defines.

Looking at our interface up here, we can see that we have a property called x and a property called y. We can replace key off point by this union type. Next up we're going to take care of the IN keyword. For every property p in this union type, we want one transformation.

The result of IN rolling in would be this. Finally, we have to resolve the two lookup types. The x property and the point type has type number. So does the y property. Here we go. This is our resolved read-only point type.

Another mapped type that ships with the typescript compiler is the partial mapped type. Let's define a point that is of type partial point. Partial here means that all properties are made optional. We can define a point that has both an x and a y property.

We can also define a point that only defines x or that only defines y or maybe even a point that defines neither. If I jump to the definition of partial t, you're going to see a similar formula. For every property p that we find in t, we want to keep the original type, but we want to make the property optional.

Of course, we can also define our own map types. Let me give you an example of a type Nullable<T>. Our transformation would look as follows. For every property p that we find in key off t, we want to keep the original type, but also union in the type null.

If I now create a variable of type nullable point, like this, I can assign either numbers or the value null to its properties. For instance I can still say "x zero, y zero," but I can also make the y property null. I can do the same for x.

Note that I cannot leave out any properties. This would not be a valid nullable point type. I would get a type error saying that property y is missing. That is because y is not optional. Note that I also can't specify properties that don't appear in the type.

In this case, the compiler would give me an error and say that object literals may only specify known properties. There's a little trick that you can use to see the effects of transforming the point type with nullable. That is to extract the type alias.

If I were to call this type nullable point, like this, I can hover over its definition and see the final result. Here we see two properties, x and y, both of type number or null. There are some really interesting things we can do with map types.

For example, we can combine their effects. I can also add the partial type here. Now we have a partial nullable point. The resulting type looks like this. We still have two properties x and y. But now we also have the type undefined unioned into this type and both properties are optional.

This means we can now leave out a property entirely or we can set one of the properties to undefined explicitly. Let's define one final map type to give you one more example. I'm going to define type stringify of t. This time, I want to turn every property type into a string.

For every property p in key off t, I want to say that the resulting type is string. We can now see that the resulting type has replaced number by string. Of course, now our definitions down here are no longer type-correct, because we still tried to assign numbers. We could fix that by assigning strings. I don't know whether that's terribly useful, but it works.