Beginner's Guide to TypeScript Mapped Types (With Code Examples)

Jayson Lennon
Jayson Lennon
hero image

Did you know that TypeScript allows you to map over a type like you would with an array?

Mapped types makes this possible and unlocks huge potential for transforming your types in a way that reduces code duplication while also enhancing the maintainability of your programs.

In this guide I’ll walk you through exactly how mapped types can makes your code more flexible and maintainable.

So grab a coffee and a notepad, and let’s dive in!

Sidenote: If you're struggling to understand mapped types and how to use them, want to learn more, or simply just want a deeper dive into TypeScript, then check out my complete course on Typescript here.

learn typescript

It takes you from a complete beginner to building real-world applications and being able to get hired, so there's something for every level of TypeScript user.

Check it out above, or watch the first videos here for free.

With that out of the way, let's get into this guide...

What are Mapped Types in TypeScript?

Mapped types are built using TypeScript's type system and they allow you to create new types based on existing ones. They work by 'mapping' properties of one type to another type.

It's a lot like iterating over an array of objects, but we are iterating over types instead. Since it's also a transformative operation, we can modify characteristics of the types, such as marking it readonly or changing it to something else entirely.

Mapped types operate on all properties of a given type, so they offer a convenient way to create types without having to duplicate every property.

For example

Here we can create a mapped type that has the same properties and types of the original:

// Here's our existing type
type OriginalType = {
  propertyA: string;
  propertyB: number;
};

// And here's our mapped type
type MappedType = {
    [ K in   keyof OriginalType ]: OriginalType[K];
//  | (A)    |______(B)_______| |  |_____(D)_____|
//  |                           |
//  |____________(C)____________|
};

So what's happening?

In the code above, MappedType will have the same properties as OriginalType. The syntax inside the square brackets is key to how mapped types work, but it can be bit daunting at first so let's break it apart:

  • (A) K in: This will create a new key (or property) from a collection of provided properties. The K is generic and any letter may be used
  • (B) keyof OriginalType: The keyof keyword will provide us with all keys (or properties) of OriginalType. In this case, it will give us propertyA and propertyB
  • (C) [K in keyof OriginalType]: Enclosing the entire statement with square braces will iterate over properties in OriginalType (so, propertyA and propertyB) and assign them to K

At this point we can imagine that we have a code fragment that looks something like this:

type ImaginaryType = {
  propertyA
  propertyB
}

However, we are still missing the types for each property. Those are filled in using the types from OriginalType by accessing them using K on part (D): : OriginalType[K].

This gives us a complete mapped type with properties and keys:

type ImaginaryType = {
  propertyA: string;
  propertyB: number;
}

Note that ImaginaryType doesn't actually exist. It is used only to illustrate what the final type will look like had you manually written it. The ImaginaryType will be generated automatically on-demand (like when you hover over the type in your IDE) and is used exclusively for type checking.

Why use Mapped Types in TypeScript?

You might be wondering why you should even care about mapped types. After all, isn't TypeScript just about adding static types to JavaScript? Why do we need to modify types and create new ones based on existing types?

Here's a few reasons why you might want to use mapped types:

  1. Reducing code duplication: Instead of redefining the same set of properties for multiple types, mapped types allow you to define the properties once and then create new types based on the original one. This significantly reduces the amount of boilerplate code while enhancing code maintainability

  2. Making code more flexible: Mapped types allow you to modify types on-the-fly. Want to make all properties of a type optional? Or readonly? Mapped types have you covered

  3. Enhancing type safety: By creating new types based on existing ones, you ensure that any changes to the original type are automatically reflected in the mapped type. This results in a more maintainable codebase where type changes are propagated throughout the application

Let's go over an example showing how mapped types can be used to reduce code duplication while also making the program more reliable.

Imagine a form submission system with 3 steps:

  1. Data entry
  2. Review
  3. Submission

During step 2 (review), we don't want the user to edit any information. This is strictly for them to confirm that the previously entered data was correct.

We can create a mapped typed that doesn't allow editing of the form data like so:

// This is our initial user-editable type
type FormData = {
  name: string;
  age: number;
};

// We create a mapped type with all the properties from the form
type ReadonlyFormData = {
  // We add the readonly keyword so the properties cannot be edited
  readonly [K in keyof FormData]: FormData[K];
};

On the step 1 (data entry) page we can use FormData and the user will be able to fill out the form and make edits.

However, once they go on to step 2 (review), we can switch over to the ReadonlyFormData which adds the readonly keyword to the properties. This prevents us from writing any code that would allow the user to make edits to the data.

TL;DR

Using mapped types in this way make your code safer, more flexible, and more maintainable. That's a win in any developer's book.

Mapped types get even better when we combine them with other TypeScript features.

Exploring Indexed Access Types and Index Signatures

It's time to talk about two important concepts in TypeScript - indexed access types and index signatures. Understanding these is essential to fully grasp the power of mapped types.

First, let's talk about indexed access types.

Index Access Types in TypeScript

Indexed access types enable you to reference the type of a property in an object. The syntax is similar to how you would access the value of a property within an object in JavaScript: foo['property'].

For example

type Person = {
  name: string;
  age: number;
};

// Indexed Access Types
type PersonName = Person['name']; // string
type PersonAge = Person['age']; // number

In this example, PersonName is of type string, and PersonAge is of type number.

We're able to access the types of the properties name and age on Person using the indexed access operator ([]). Now we can use them like we would any other type, and any changes to Person will be reflected elsewhere:

type Student = {
  name: PersonName, // string
  age: PersonName, // number
}

Next, we have index signatures.

Index Signatures in TypeScript

An index signature allows you to define the types of "indexable" properties, i.e., the properties that could be accessed using a numeric or string index.

For example

type Receipt = {
  [index: string]: number;
};

In this case, Receipt can have any number of properties with string names, and the type for each of those properties must be a number.

This is useful when you need to enforce specific types in an object, like so:

const groceries: Receipt = {
  apples: 3.30,
  bread: 1.20,
  juice: "2.15", // ERROR: string not assignable to number
};

Combining indexed access types and index signatures with mapped types can lead to some powerful and flexible type definitions, capable of handling complex problems.

A quick recap: TypeScript Utility Types

TypeScript comes with utility types that provide convenient type transformations and can be utilized in various scenarios, supplementing nicely with mapped types.

Remember, the beauty of mapped types lies in their ability to create new types based on existing ones, and utility types enhances this by offering commonly used transformations out-of-the-box.

One of the most common utility types used in conjunction with mapped types is Partial<T>. What Partial<T> does is essentially create a new type with all the properties of the input type T made optional.

So, if you think about it, Partial<T> is actually a mapped type!

Here's an example of Partial<T> in action:

type Person = {
  name: string;
  age: number;
};

type PartialPerson = Partial<Person>;

As you can see, PartialPerson is a new type where name and age properties are optional.

But what if you wanted to make all properties of a type read-only?

Well, there's a utility type for that too! It's called Readonly<T>, and it looks like this:

type Person = {
  name: string;
  age: number;
};

type ImmutablePerson = Readonly<Person>;

ImmutablePerson is now a type with the same properties as Person, but you can't change them because they're marked readonly.

There are several other utility types available, such as Record<K,T>, Pick<T,K>, Omit<T,K> and more, each serving a unique purpose.

What's next?

And there we have it - an introductory guide to understanding and working with mapped types in TypeScript.

But remember, reading is just the first step. The real mastery comes from practice!

So why not try experimenting with mapped types in your next project?

You could:

  • Identify a type that's being reused or duplicated and convert it into a mapped type
  • Or even take a complex function that accepts many different argument types and see if a mapped type can simplify it

However, do keep in mind - there's no one-size-fits-all solution. Mapped types are just another tool in your TypeScript toolbox, and like all tools, they're most effective when used appropriately. Don't be afraid to experiment and adjust to find the solution that fits your project best.

P.S.

If you want to learn more, check out my complete TypeScript course here on Zero To Mastery, or watch the first videos for free.

learn typescript

Like I said earlier, it can take you from a total beginner with basic JS experience, to building large scale apps and being able to be hired as a TypeScript developer.

You'll not only get the course content, you'll also get direct access to me, as well as other devs who are all learning TS, in our private Discord server!


You can ask questions, get feedback, or simply chat shop with other working developers and students!

BONUS: More TypeScript tutorials, guides & resources

If you've made it this far, you're clearly interested in TypeScript so definitely check out all of my other TypeScript posts and content:

More from Zero To Mastery

TypeScript Interview Questions + Answers (With Code Examples) preview
TypeScript Interview Questions + Answers (With Code Examples)

Boost your TypeScript interview prep with 42 key questions. Learn thorough answers and examples to confidently land your next tech job.

TypeScript vs. JavaScript… Which Is Better and Why? preview
TypeScript vs. JavaScript… Which Is Better and Why?

You should 100% learn both. But these are the key pros and cons of each (with code examples) so you know when best to use each of them for your next project!

Type Checking In TypeScript: A Beginners Guide preview
Type Checking In TypeScript: A Beginners Guide

What if you could catch errors at both compile and runtime? Thanks to TypeScript's type checking feature, you can! Learn how in this guide, with code examples.