Skip to content

Understanding Mapped Types In TypeScript

Oct 29, 2022 · 5 min read

In this post, we’ll look at Mapped Types in TypeScript.

It’s recommended that you have a basic understanding of generics first before proceeding.

What Is Mapped Type?

A mapped type is a generic type which uses a union of PropertyKeys (frequently created via keyof) to iterate through keys to create a new type.

Mapped type is easier to explain using examples. Say that we have an interface that look like this:

interface RequireAB {
a: number;
b: string;
}

Any object that implements the interface must have the a and b properties.

const obj: RequireAB = {
a: 1,
b: "hello",
};

Now, what if we want to make another interface that doesn’t require the a and b properties. We can accomplish it this way.

interface NotRequireAB {
a?: number;
b?: string;
}

This look awfully familiar with the RequireAB interface above. Can’t we just create our NotRequireAB based on RequireAB? Of course we can, using Mapped Type.

interface RequireAB {
a: number;
b: string;
}
const obj: RequireAB = {
a: 1,
b: "hello",
};
type NotRequireAB<RequireAB> = {
[K in keyof RequireAB]?: RequireAB[K];
};
const obj2: NotRequireAB<RequireAB> = {};

We created NotRequireAB based on RequireAB. The type will contain a property for each property in RequireAB but with an optional modifier (?) next to it. We’re basically just creating this code dynamically:

type NotRequireAB = {
a?: number;
b?: string;
};

An advantage of using this method to hard-coding is that we don’t need to maintain 2 separate types. In the case where we have to add some more properties to RequireAB, we don’t need to do it for NotRequireAB if we created it using mapped type. This will make the code more maintainable.

interface RequireAB {
a: number;
b: string;
c: boolean;
d: string | number;
}
// We don't need to do any modification here
type NotRequireAB<RequireAB> = {
[K in keyof RequireAB]?: RequireAB[K];
};

We can make the type even more powerful with generics.

type WeakInterface<T> = {
[K in keyof T]?: T[K];
};

WeakInterface is a type that take in a type T and make all the properties optional.

We can use WeakInterface whenever we need to use an interface but with all the properties optional.

const optional: WeakInterface<RequireAB> = {};

Note that we still can’t define properties that are not available on the original type.

const willError: WeakInterface<RequireAB> = {
c: "test";
}

The code above will produce the following error:

Type '{ c: string; }' is not assignable to type 'WeakInterface<RequireAB>'.
Object literal may only specify known properties, and 'c' does not exist in type 'WeakInterface<RequireAB>'.

Adding And Removing Modifiers

We can add or remove modifiers by prefixing them using the + or - operator.

When no operator is specified, TypeScript will assume that it’s +. That’s why on our WeakInterface above, we don’t need to specify + before our ? modifier.

Now let’s look at an example where we use the - operator. Say that we want to make our WeakInterface strong again. We can remove all the ? modifier like this:

type StrongInterface<T> = {
[K in keyof T]-?: T[K];
};

All we have to do is add the - operator before the modifier.

Introducing Some Predefined Mapped Types

Transforming properties with mapped types is a very common operations that TypeScript provides us with some predefined type definitions.

You can check them out on lib.es5.d.ts.

In this section, we’ll explore some of these type definitions.

Partial

Partial make all properties of a type optional.

/**
* Make all properties in T optional
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};

Our WeakInterface we implemented before is actually a Partial.

Required

Required is the opposite of Partial. It will mark each property as required.

/**
* Make all properties in T required
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};

Readonly

We can use Readonly mapped type to mark each property as readonly (immutable), as follows:

/**
* Make all properties in T readonly
*/
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Readonly is a mapped type that adds a readonly property to each properties of the type, making the type immutable.

We can also create a type that remove the readonly modifier, and make the properties mutable again.

type CreateMutable<T> = {
-readonly [K in keyof T]: T[K];
};

Now, Let’s look at an example of using Readonly.

interface RequireAB {
a: number;
b: string;
}
const obj: Readonly<RequireAB> = {
a: 1,
b: "hello",
};
obj.a = 2;

The last line will produce an error as expected.

Cannot assign to 'a' because it is a read-only property.

Pick

Pick is used to construct a type based on a subset of properties of another type.

/**
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};

Let’s look at an example that use Pick.

interface ABC {
a: number;
b: string;
c: boolean;
}
let obj: Pick<ABC, "a" | "b"> = {
a: 1,
b: "hello",
};

In obj, we only need to specify the properties a and b. Property c doesn’t exist on the type generated by Pick.

Record

Record is used to construct a type on the fly. In a way, it’s the opposite of Pick. Record uses a list of properties as a string literal to define what properties the type must have.

/**
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};

Let’s look at an example that use Record.

let obj: Record<"c" | "d", string> = {
c: "hello",
d: "world",
};

The type in our obj is created by passing two generic arguments to the Record type. Record then create a new type with the properties of c and d, both of type string.

Wrap Up

That’s it for Mapped Types 🎉. Hope you find it useful.