Breaking Down a Complex Mapped Type
Tags: typescript, tutorial, webdev
Published: 2022-05-29 22:14:35

There is a useful generated type pattern in TypeScript called a [_mapped type_](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html). Let's make sense of it by starting from first principles and building up to the full example

---

While watching [Matt Pocock](https://twitter.com/mattpocockuk)'s video on [Using DECLARE GLOBAL for amazing type inference](https://www.youtube.com/watch?v=byCYSXVmH6E), I was pretty stuck trying to make sense of the type of `event` in the `GlobalReducer`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i1g0yzdkxb80wd4bkcb4.png)

There are a lot of pieces to this type and, at least for me, none of them are particularly intuitive. For me to make sense of it, I needed to break it down into smaller pieces first.

Here is a walkthrough of that process.

For starters, here is a `GlobalReducerEvent` that we can add and remove reducer types from.

```typescript
interface GlobalReducerEvent {
    ADD_TODO: {
        text: string
    }
    LOG_IN: {
        email: string
    }
    DELETE_TODO: {
        todo_id: number
    }
}

type ReducerEvent = ... /* what we're about to build */

type GlobalReducer<TState> = (
    state: TState,
    event: ReducerEvent
) => TState
```

We want our `GlobalReducer` to do type checking based on what events are included. For that we'll need to use some fun TypeScript features that allow us to map the `GlobalReducerEvent` into a type that is useful in our user-land code.

```typescript
// this is our target type
type ReducerEvent = ({
    type: "ADD_TODO";
    text: string;
}) | ({
    type: "LOG_IN";
    email: string;
}) | ({
    type: "DELETE_TODO";
    todo_id: number;
})
```

Building up to the `ReducerEvent` (the type of `event`) is our end goal here.

Starting with [`keyof`](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html), we can get a union type of all the events.

```typescript
// What are our different types of events?
type EventUnion = keyof GlobalReducerEvent;
/*
type EventUnion = 'ADD_TODO' | 'LOG_IN' | 'DELETE_TODO'
*/
```

That is a crucial piece of the puzzle as it is used in two different places in the end result.

The next few steps are going to be building up an intermediate type of sorts. A type that will help us map the original type into our target type.

With `in`, we can create an index signature over the values from `EventUnion` (which remember is `keyof GlobalReducerEvent`). For the moment, we'll type each of those keys as `any`.

```typescript
// Gather up the types of events
// (we'll replace the `any` in a moment)
type EventTypes = {
    [EventType in EventUnion]: any
};
/*
type EventTypes = {
    ADD_TODO: any;
    LOG_IN: any;
    DELETE_TODO: any;
}

same as doing:

type EventTypes = {
    [EventType in keyof GlobalReducerEvent]: any
}
*/
```

Now instead of typing them as `any`, let's key them to an object. This object is going to start building toward the values we are trying to map to.

Each event should have a `type` whose value matches its name. `EventType` is a reference to the name of the event, so we can use that in the object type (`{ type: EventType }`).

```typescript
// Event type keyed to itself as an object
// (weird intermediate type, stick with me here)
type EventTypesWithSelf = {
    [EventType in keyof GlobalReducerEvent]: {
        type: EventType
    }
};
/*
type EventTypesWithSelf = {
    ADD_TODO: {
        type: "ADD_TODO";
    };
    LOG_IN: {
        type: "LOG_IN";
    };
    DELETE_TODO: {
        type: "DELETE_TODO";
    };
}
*/
```

Each event has typings defining the data that needs to go with it. For instance, `ADD_TODO` requires a bit of text that constitutes the thing to be done. Hence, `ADD_TODO: { text: string }`. In this next part, we are going to fold in the typings for each event's data.

We can use the [intersection type literal (`&`)](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#intersection-types) to combine each event type (`{type: 'ADD_TODO'}`) with its data typing (`{text: string}`).

```typescript
// Event type keyed to itself and its data
// (still looks weird, but starting to take shape)
type EventTypesWithSelfAndData = {
    [EventType in keyof GlobalReducerEvent]: {
        type: EventType
    } & GlobalReducerEvent[EventType]
};
/*
type EventTypesWithSelfAndData = {
    ADD_TODO: {
        type: "ADD_TODO";
    } & {
        text: string;
    };
    LOG_IN: {
        type: "LOG_IN";
    } & {
        email: string;
    };
    DELETE_TODO: {
        type: "DELETE_TODO";
    } & {
        todo_id: number;
    };
}

which you can think of as:

type EventTypesWithSelfAndData = {
    ADD_TODO: {
        type: "ADD_TODO";
        text: string;
    };
    LOG_IN: {
        type: "LOG_IN";
        email: string;
    };
    DELETE_TODO: {
        type: "DELETE_TODO";
        todo_id: number;
    };
}
*/
```

Before we get to the last part, let's wrap our heads around [indexed access](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) of a type object.

The works a lot like accessing the value for a key from a JavaScript object. Using the type from the previous step, we are able to grab just the `'ADD_TODO'` type.

```typescript
// Let's try an indexed access of EventTypesWithSelfAndData
type AddTodoType = EventTypesWithSelfAndData['ADD_TODO']
/*
type AddTodoType = {
    type: "ADD_TODO";
} & {
    text: string;
}
*/
```

Where it differs from how JavaScript object access works is evidenced by passing a union type instead of an individual value.

```typescript
// What if we try to access multiple types at once with a union
type SomeEventTypes = EventTypesWithSelfAndData['ADD_TODO' | 'DELETE_TODO']
/*
type SomeEventTypes = ({
    type: "ADD_TODO";
} & {
    text: string;
}) | ({
    type: "DELETE_TODO";
} & {
    todo_id: number;
})
*/
```

Notice the resulting type is a union type made up of just the types indexed at the union values.

We can take this a step further by doing an indexed access with `keyof GlobalReducerEvent` which is a union of all our event types.

```typescript
// That means we can pass in a union of all our event type names
// to get a union of all the type signatures.
type AllEventTypes = EventTypesWithSelfAndData[keyof GlobalReducerEvent]
/*
type AllEventTypes = ({
    type: "ADD_TODO";
} & {
    text: string;
}) | ({
    type: "LOG_IN";
} & {
    email: string;
}) | ({
    type: "DELETE_TODO";
} & {
    todo_id: number;
})

which, you might remember, is equivalent to this:

type AllEventTypes = ({
    type: "ADD_TODO";
    text: string;
}) | ({
    type: "LOG_IN";
    email: string;
}) | ({
    type: "DELETE_TODO";
    todo_id: number;
})
*/
```

That last example is equivalent to this full ReducerEvent. We built it up from all the constituent parts. Fun!

Let's take another look at the whole thing again.

```typescript
type ReducerEvent = {
    [EventType in keyof GlobalReducerEvent]: {
        type: EventType
    } & GlobalReducerEvent[EventType]
}[keyof GlobalReducerEvent]
```

Hopefully having walked through all the different pieces of this, it is looking less like magic and more like something you can reason about.

_If you enjoyed this post, consider [joining my newsletter](https://crafty-builder-6996.ck.page/e169c61186) or following me on [twitter](https://twitter.com/jbrancha). If this helped you or you have a question, feel free to drop me note wherever. I'd love to hear from you!_

Want to play around with this in more detail? Check it out in [the TypeScript Playground](https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgOIBsD2AjO6BKEAJgK5JQCiAbhOMgN4BQyLyAggCIcD6AKgPId+ALgbNWEyAA8wogM5gooAObiWAXzXIAMv1TcAkgDlRTCRIgBbOMHTzFKrZokcK2irwp9BIsedZgmESY3MBEoiAkltjQToyajAD0AFTIvAAW0CjAcshwyFhgyMqYKsiYIMiZUBAANAUQYADkudg1cADWyMBFwQDuIAB0w4xgAJ4ADiiEpOTUtEUAvH6sANrz4LyT2ZUdEGOYMGhYuATEZNAbYAC6plqS26JXW1NOyABkxzh4MxeUNOB1gCwC8INd4qs9gcjhhvmdZpdgeDkolGElEsgAOrpOBFOA1cokKDIIjAGAwLJ0cZTXKHZAQYFyAD8o22yCuAFUQMAKshllC6bDTr85sCANxJZKsqbs4FcnmVZZNTg8ARCJrIAA+yCaun0xg12qarncnm86sYKLRiQxqFx1WQJAmyDAmRd21pRwZCzk6OQAAo+hAmuh0MgahN0IgUK6UAADOAgMZx7qVfKWTCWBYASmlKGeHr5KxYQIWoNTsoW8oqtzySfiEpSecrm0LyzMrBV5t8ibGEokesMJjrfa0Jo8XjVPaTEoScjgWbyuWCKmEaOp+eBoNy7a0pdbMtAyAFMJOP3OooWtd78UtqL9V3dMqhxBdmG6YDkEHQRzguUT5TYAAVhACBgH6gYQMAUBEKmkBQFmpK4jG2z1AowAIF0fQ9OkyCLtUEC5huLYgh6mI4QAyt+Ry7hI+6kYeuz7IKZ7wn8Vy1h2-gbk8W7bHE6iNlKxEFjS5GulRP5Flx7BcN2dz+CwPHIAARF2U4qf2rCCVog7GApinKSpelGJpThaSw45mlOBncY8qlWZOPhmRIOmaFaD7Ak+KAvrBgQfl+UmJrBPTLricAQehoYFJgmAdLkQbQUQ9TYCQRQKPiYBlP5YCdCgcg4lMRFsqJEByOJ6SSTAbAgEQHDhdJe6lRWJ5fMKF6IletnmMppVvJ8Qrngi-wLPRoLgjpTYiXxYmUdRNV1Q1tGdnJNnFj19lqatzkWcg6gfOtkgQDI9hKCAyi7TpA56EO3UPFMojGTdxgudpB0yRY1i2KdKiXbtjnyYdASbQDGmXe99wBEEIRhBEUQxFAf23owfTpBhuEHCQyAIABrqgF0dJ-muzaleVc0-gt9W5Y1EjqT4d3Aw9ql00Ir3ccdsjIAoZ0XeZunPcOH2MxAj0mWz5hWDYdhcw451Iy4bgToDQtKSDivWTtkNKdDoThMgkTRNASPufeNo6I0LQulAYx1qmRDHa+iBIHInokduFVVZT4XNmwRBELw0PlsspMe-NtVU3AqzKtt6rglNbK+-7gdsstqtM1tqqa-tnwydInPc79t4eWb2K4t0RxBlbNv+U7ZW5JYJDoFlkYoTSeRFBUSDINhrp5I63IVM2FGZhApNFiH5PVeH4VRyz-CGjqoM+E0cfCWyw9ZmPyz+rnm1z2Z2frXnP1y-E2ZagGu-p0vrOzhDkg67D+vw0bZ93taGIZGXWaJglKA45UCYf5chHnyCQAelRCbRUwESekXliIgAXGVP0-llCND7uAhU5RfzRVjN5LmwBlCILAESMqgwIKYn-gBEgX5kBxhPCmGAmBiR41yN6EAABCYqMo2Chi3m7Mik8va5UhMxU8cIRSdXAKvH2fCZplSLDvLQRl953xzsojmJ9ebqHPtqJR90RaqTFmo9aktvoyx5rOXRl9lFq1NE5W+8R75Q2CLrOGhtEZvxRmjBA6R6iYzwoQ9IRQahZg8fUHI9IACOJBgBUDwAsN8Lo0ZyGJsRXh6B+H6OFo9VRGiToWMLjoi+2S06GKevqUyu0zHSwLqfYpeir7lJvvwcWgRXFPwNgjKx78-RfyKFGBQ9IpALhbt0NhMS4kJKpO+VhyAYCNzDJIka4ByElxQKlWwRQeiOmdDAKAmY8i4LdAgCo6ESGJKAVAT8gxkAADFwEcObMsx8qdkBjTZEeVqg02KXmkQzMpvEyz8VcgdH5LzgQfKmBNUR0I2pDXYkiNEKC2Tgo6lAAAPLwCiuVIAAD5FF+gkBlSAohsW4rqES1g7DOYQoWH6c+iwCXkuQh-bGZyijtMwFcUQdK6Dtm8qIaOmd1T1GPovd8UZwFEGthqBIZtTkgCGZYMYyzRBouGhinAIEwIEu3iSuocCcx8gJUwM2EhFVyEwOgCAgwsDKH9DS7MjYMQSBqCQqAlQDUur2mylVyyd7qDFdDK4zqgA).

---

Cover photo by [GeoJango](https://unsplash.com/@geojango_maps?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) Maps on Unsplash