TypeScript mapped types. Part 1.

This blog is a part of my TypeScript series, and the previous ones are:

1. Why program in TypeScript
2. Structural vs nominal typing
3. Getting started with TypeScript classes
4. Access modifiers public, private, and protected
5. Abstract classes
6. enums
7. An intro to TypeScript generics

NOTE: Understanding of the generics syntax is a pre-requisite for understanding this blog.

NOTE: If you’re reading this blog before March 20, 2019, you can buy our book “TypeScript Quickly” (it’s in MEAP now) at 50% off using the code wm031519lt here.

Mapped types allow you to create new types from the existing ones. This is done by applying a transformation function to an existing type. In this blog, you’ll see how they work by looking at the type Readonly that comes with TypeScript. In the next blog, I’ll show you how to create your own mapped types.

The mapped type Readonly

Imagine that you need to pass the objects of type Person (shown next) to the function doStuff() for processing.

interface Person {
  name: string;
  age: number;
}

This class is used in multiple places, but you realized that you don’t want to allow the function doStuff() to accidentally modify some of the Person’s properties like age.

const worker: Person = { name: "John", age: 22};

function doStuff(person: Person) {

    person.age = 25; // We don’t want to allow this
}

None of the properties of the type Person was declared with the readonly modifier. Should we declare another type just to be used with doStuff() as follows?

interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}

Does it mean that you need to declare (and maintain) a new type each time when you need to have a read-only version of the existing one? There is a better solution. We can use a built-in mapped type Readonly to turn all the properties of a previously declared type to be readonly. We’ll just need to change the signature of the function doStuff() to take the argument of type Readonly<Person> instead of Person, just like this:

const worker: Person = { name: "John", age: 22};

function doStuff(person: Readonly<Person>) { 

    person.age = 25;   // This line generates a compiler error
}

To understand why an attempt to change the value of the property age generates a compiler error, you need to see how the type Readonly is declared, which in turn requires an understanding of the lookup type keyof.

The lookup type keyof

Reading the declarations of the built-in mapped types in the file typescript/lib/lib.es5.d.ts (it comes with TypeScript installation) helps in understanding their inner-workings and requires familiarity with the TypeScript’s index type query keyof a.k.a. a lookup type.

You can find the following declaration of the Readonly mapping function in lib.es5.d.ts:

type Readonly = {
readonly [P in keyof T]: T[P];
};

We assume that you’ve read about generics in chapter 4, and you know what in angle rackets means. Usually, the letter T in generics represents type, K – key, V – value, P – property et al.

keyof is called index type query or a lookup type. and it represents a union of allowed property names (the keys) of the given type. If the type Person would be our T, then keyof T would represent the union of name and age. The next screenshot was taken while hovering the mouse over the custom type propNames. As you see, the type of propName is a union of name and age.


In the previous listing, the fragment [P in keyof T] means “give me the union of all the properties of the given type”. This seems as if we’re accessing the elements of some object, but actually, this is done for declaring types. The keyof type query can be used only in type declarations.

Now we know how to get access to the property names of a given type, but to create a mapped type from the existing one, we also need to know the property types. In case of the type Person, we need to be able to find out programmatically that the property types are string and number.

This is what lookup types are for. The piece T[P] is a lookup type, and it means “Give me the type of a property P”. The next screenshot was taken while hovering the mouse over the type propTypes. The types of properties are string and number.

Now let’s read the code in the previous listing  one more time. The declaration of the type Readonly <T> means “Find the names and types of the properties of the provided concrete type and apply the readonly qualifier to each property”.

In our example, Readonly<Person> will create a mapped type that will look like this:

interface Person {
readonly name: string;
readonly age: number;
}

Now you see why an attempt to modify the person’s age results in the compiler’s error “Cannot assign to age because it’s a read-only property”. Basically, we took an existing type Person and mapped it to a similar type but with the read-only properties. You can try this code in the Playground.

You may say, “OK, I understand how to apply the mapped type Readonly, but what’s the practical use of it?” Those of you who purchased our book “TypeScript Quickly”  will see plenty of examples of using Readonly while going through the code of a sample blockchain app that comes with the book. For example, in chapter 10 in listing 10.15 you can see two methods that use the Readonly type with their message argument:

replyTo(client: WebSocket, message: Readonly<T>): void

This method can send messages to blockchain nodes over the WebSocket protocol. The messaging server doesn’t know what types of messages are going to be sent, and the message type is generic. To prevent accidental modification of the message inside replyTo(), we use the mapped type Readonly there.

In the next blog, I’ll show you how to create your own mapped types.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s