TypeScript mapped types. Part 2

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
8. TypeScript mapped types. Part 1

In the previous blog, I showed you how the transformation function for the built-in mapped type Readonly was declared in the file typescript/lib/lib.es5.d.ts:

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

You can define your own transformation functions using similar syntax. Let’s try to define the type Modifiable – an opposite to Readonly.

We took a type Person made all of its properties read-only by applying Readonly mapped type: Readonly. Let’s consider another scenario. Say, the properties of the type Person were originally declared with the readonly modifier as follows:

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

How can you remove the readonly qualifiers from the Person declaration if need be? There is no built-in mapped type for it, so let’s declare one:

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

The minus sign in front of the readonly qualifier removes it from all properties of the given type. Now you can remove the readonly restriction from all properties by applying the mapped type Modifiable:

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

const worker1: Person = { name: "John", age: 25};

worker1.age = 27;  // compile error

const worker2: Modifiable = { name: "John", age: 25};

worker2.age = 27;  // No errors here 

You can see this code in the Playground at https://bit.ly/2GMAf3c.

Other built-in mapped types

You know that if a property name in the type declaration ends with the modifier ?, this property is optional. Say we have the following declaraion of the type Person:

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

Since none of the Person’s properties names ends with a question mark, providing values for name and age is mandatory. What if you have a need in type that has the same properties as in Person, but all of its properties should be optional? This is what the mapped type Partial is for. Its mapping function is declared in lib.es5.d.ts as follows:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

Have you spotted the question mark there? Basically, we create a new type by appending the question mark to each property name of the given type. The mapped type Partial makes all properties in the given type optional. The following screenshot was taken while I was hovering the mouse over the declaration of the worker1 variable.

It shows an error message because the variable worker1 has the type Person, where each property is required, but the value for age was not provided. There are no errors in initializing worker2 with the same object because the type of this variable is Partial, so all its properties are optional.

There is a way to make all properties of a type optional, but can you do the opposite? Can you take a type that was declared with some optional properties and make all of them required? You bet! This can be done with the mapped type Required that’s declared as follows:

type Required<T> = {
    [P in keyof T]-?: T[P];  // The -? means remove the modifier ?.
};

The next screenshot was taken while I was hovering the mouse over the declaration of the worker2 variable. The properties age and name were optional in the base type Person but are required in the mapped type Required hence the error about missing age.

Tip: The type Required was introduced in TypeScript 2.8. If your IDE doesn’t recognize this type, make sure it uses the proper version of the TypeScript language service. In Visual Studio Code you can see its version in the bottom right corner. Click on it to change to a newer version if you have it installed.

You can apply more than one mapped type to a given type. In the next listing, I apply Readonly and Partial to the type Person. The former will make each property read-only and the latter will make each property optional. There I initialize the property name but not the optional age. The property name is read-only and can be initialized only once.

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

const worker1: Readonly<Partial<Person>>   
                       = { name: "John" }; 

worker1.name = "Mary"; // compiler's error 

TypeScript offers yet another useful mapped type called Pick. It allows you to declare a new type by picking a subset of properties of the given type. Its transformation function looks like this:

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

The first argument expects an arbitrary type T, and the second – a subset K of the properties of this T. You can read it as “From T, pick a set of properties whose keys are in the union K”. The next listing shows the type Person that has three properties. With the help of Pick, we declare a mapped type PersonNameAddress that has two string properties: name and address as seen in the following listing.

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

type PersonNameAddress<T, K> = Pick<Person, 'name' | 'address' >; 

The moral of the mapped types story? Mapped types allow you to create apps that have a limited number of basic types and many derived types based on the basic ones.

One thought on “TypeScript mapped types. Part 2

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