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.
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 instead of Person, just like this:
const worker: Person = { name: "John", age: 22}; function doStuff(person: Readonly) { 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 type keyof.
The 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.
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 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 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 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): 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.
One thought on “TypeScript mapped types. Part 1.”