Typescript's Immediately Indexed Mapped Type (IIMT)

๐Ÿฅช๐Ÿงƒ๏ธ 15 mins read

The concept "Immediately Indexed Mapped Type (IIMT)" was coined by Matt Pocock to describe a common pattern in Typescript.

I'm going to show you an example and hopefully will explain how it works and why it's so useful.

Mapped types

Let's start with a simpler example. What is a mapped type? I'm sure you're already familiar with the concept of "mapping" and it will remind you of Array.prototype.map(). It's more or less the same concept but applied to types: you use a simple type or an interface and create another one, mapping the first one (doing some kind of transformation to the original one).

Let's see an example.

type 
Foo
= {
bar
?: string
baz
?: number
} type
FooMappedAsBoolean
= {
[
K
in keyof
Foo
]: boolean
} const
FooBoolean
:
FooMappedAsBoolean
= {}
// All the properties // in `FooBoolean` // are now of type boolean!

Tap on FooMappedAsBoolean to hide the popover ๐Ÿ˜‰. Or click on the popover.

I'll explain what's happening in this example.

We're converting all value types in Foo from their original type to boolean. This is achieved by using a mapped type, which transforms the value types.

Just like in Array.prototype.map() in JS, we iterate the original type's properties using K in keyof .... You can think of [keyof Foo] as Object.keys(Foo) but in TypeScript.

Mapped Types With Generics

We often see mapped types used with generic types (also known as "parameter types"), to make them reusable and more flexible. Let's take a look at an example:

type 
Foo
= {
bar
?: string
baz
?: number
} // Commonly in TypeScript we use `T` for the generic name export type
AnythingMappedAsBoolean
<
T
> = {
[
K
in keyof
T
]: boolean
} const
FooBoolean
:
AnythingMappedAsBoolean
<
Foo
> = {}

This example is similar to the previous one, but in this case, AnythingMappedAsBoolean accepts a parameter type called T. This means that we can use it with any type and reuse it for different types of objects.

You can think of generics as function parameters in JavaScript.

From here, things can get more complex. If you want to keep on exploring mapped types, I recommend starting with the official docs and I also recommend this chapter in Matt Pocock's Essentials book.

What is indexing in TypeScript?

Now that we know what a basic "mapped type" is, let's break down one of the remaining long words in the title of this article: indexing. Indexing does in TypeScript the same thing you could think it does when we speak about a JavaScript object (or an array).

// indexing in JS
const obj = {
  property: 'hello',
}

obj['property'] // -> 'hello'
// in JS you can also do:
obj.property // -> 'hello'
// indexing in TS
type 
MyType
= {
property
?: string
} // we use a reference to another type accessing properties by index type
MyCustomProperty
=
MyType
['property']
// ๐Ÿ’ฅ but you cannot do: `MyType.property` // That kind of syntax is reserved for "namespaces"

We can also use this term to access Arrays ("tuples") of types:

type 
MyTwoTypes
= [string, number]
type
MySecondType
=
MyTwoTypes
[1] // `number`

That's what indexing means in Typescript.

Now, let's see one of the most common indexing patterns you will see. This pattern is found with many different names. I'll use ObjectValues.

type 
ObjectValues
<
T
> =
T
[keyof
T
]
// Example: const
obj
= {
a
: 'hello',
b
: 123,
} type
MyObjectValues
=
ObjectValues
<typeof
obj
>

keyof T gives us all the keys from a given object as a union of types. With T[keyof T], we get all the values from a given object as a union of types.

In this case, it's an object with two properties: a and b. The type of ObjectValues<typeof obj> is string | number.

If there were duplicated types, you wouldn't get them in the resulting union, only the unique ones, as long as they are non-exclusionary. For example:

type 
test
= string | unknown

Immediately Indexed Mapped Types (IIMTs)

Now that we know what "indexing" means, how to map types, and also how to do it with generics, we can finally build an "Immediately Indexed Mapped Type (IIMT)".

Let's use a real-world example I came across in a Vue.js codebase:

// I had to disable `twoslash` in this snippet because it was failing.
// But there is a link to the playground right below.

/**
 * Take only the props that are mandatory from a Component
 * As a union of strings (props names)
 */
type MandatoryProps<ComponentProps> = {
  [Prop in keyof ComponentProps]-?: {} extends Pick<ComponentProps, Prop> ? never : Prop
}[keyof ComponentProps]

// Example
interface MyComponentProps {
  disabled?: boolean
  label: string
  id: string
  onClick?: () => void
}

type MyComponentMandatoryProps = MandatoryProps<MyComponentProps>
// it should give us: `'label' | 'id'`, a union of strings

Explanation

That's a complex type! I think it deserves to be broken down.

Matching optional and required props

I think it has 2 brilliant pieces that are worth discussing.

  • The -? part makes all properties mandatory, which is important later to match with the {} extends Pick<ComponentProps, Prop> part. The properties that do not match, are excluded.
  • The {} extends Pick<ComponentProps, Prop> part is a reversed condition, we use Prop (which represents each property but is made mandatory) to check if it matches the properties in the original ComponentProps. If not, then it's excluded in the final result.

The meat of this type is that we cannot match (with extends) two types using one property to compare them if the property is mandatory in one type but optional in the other.

type 
OptionalProps
= {
example
?: string
} type
MandatoryProps
= {
example
: string
} type
CanTheyBeExtended
=
OptionalProps
extends
MandatoryProps
? true : false

If you apply that to all the keys in a type, using keyof T, then it's possible to exclude properties from one type if they are not present in another.

// Let's use the same example
interface 
MyComponentProps
{
disabled
?: boolean
label
: string
id
: string
onClick
?: () => void
} // Let's focus on 1 property, it was optional originally type
onClickWhenOptional
=
Pick
<
MyComponentProps
, 'onClick'>
type
onClickOptionalProp
=
onClickWhenOptional
['onClick']
// Now let's make them all required type
AllRequiredProps
=
Required
<
MyComponentProps
>
type
onClickWhenRequired
=
Pick
<
AllRequiredProps
, 'onClick'>
type
onClickRequiredProp
=
onClickWhenRequired
['onClick']
// Can we intersect them? type
test
=
onClickWhenOptional
extends
onClickWhenRequired
? true : false // false
// ๐Ÿ’ฅ Therefore, the following should error! // @ts-expect-error type
test2
=
Pick
<
AllRequiredProps
,
onClickOptionalProp
> // {}
// And you should not be surprised by this: type
test3
=
onClickOptionalProp
extends
onClickRequiredProp
? true : false // false

Now that you know that optional properties cannot be matched with required properties, let's go back to the {} extends Pick<ComponentProps, Prop> ? never : Prop bit.

This means:

Does an empty object ({}) matches (extends) the original props (ComponentProps) with each prop made mandatory Prop? If so, (never) discard that property (Prop), or else return the property key as value.

Why never?

The never part can also be unintuitive. never helps us to remove one property in the resulting object in our example because never is removed from a union:

type 
test
= string | never
// should be `string`

What we're doing on our type is to assign never as a value when the property is not mandatory, and later, we access the object type values matching the same keys as the object type keys.

type 
source
= {
a
: string
b
: never
} type
test
=
source
[keyof
source
]
// should be just `string`, because `string | never` is resolved as `string`

And that's why we assign as values in our type never : Prop, to later get only the matching Props.

I hope it was clear after dissecting the type in smaller parts and using similar examples.

Conclusion

I hope you learned a couple of things from this article, I sure did while writing it! It reminded me of the importance of breaking a complex problem into smaller chunks so it can be understood better.

The next time someone talks about "Immediately Indexed Mapped Types (IIMTs)," "mapped types," or "indexing," you'll be in a unique position to truly understand what it means because you've taken the time to grasp the underlying concepts.

Thanks for reading and happy coding ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป!