Mapped allow types to be defined in other types through a much more flexible version of an index signature. We’ll study this type in detail, and demonstrate how it makes language features like indexed access types and conditional types even more powerful!
The basics
If you recall the concept of index signatures allow specification of some value type for an arbitrary key. They’re the foundation for dictionary types in TypeScript:
tsTry
typeFruit = {name : stringcolor : stringmass : number}typeDict <T > = { [k : string]:T } // <- index signatureconstfruitCatalog :Dict <Fruit > = {}fruitCatalog .apple
What if we didn’t want just any key to be used to store fruits
in our fruitCatalog
object, but a specific subset of keys.
You could do this with a map type. We’ll stop calling our collection
a Dict
, since that would imply that we could still use arbitrary keys. Let’s
call this a Record
instead. How about MyRecord
…
tsTry
typeFruit = {name : stringcolor : stringmass : number}// mapped typetypeMyRecord = { [FruitKey in "apple" | "cherry"]:Fruit }functionprintFruitCatalog (fruitCatalog :MyRecord ) {fruitCatalog .cherry fruitCatalog .apple Property 'pineapple' does not exist on type 'MyRecord'.2339Property 'pineapple' does not exist on type 'MyRecord'.fruitCatalog .pineapple }
The thing that looks like an index signature is what makes this a mapped type:
ts
{ [FruitKey in "apple" | "cherry"]: ... }
Let’s compare this to a true index signature so that we can see the differences
ts
{ [nameDoesntMatter: string]: ... }
Notice:
- The
in
keyword in the mapped type - Index signatures can be on all
string
s or allnumber
s, but not some subset of strings or numbers
tsTry
typeAn index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.1337An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.MyRecord = { [: "apple" | "cherry"]: key Fruit }
Record
If make our type a bit more generalized with some type params
instead of hardcoding Fruit
and "apple" | "cherry"
as shown below,
we’ll arrive at a built-in utility type that comes with TypeScript.
diff
- type MyRecord = { [FruitKey in "apple" | "cherry"]: Fruit }+ type MyRecord<KeyType, ValueType> = { [Key in KeyType]: ValueType }
tsTry
typeMyRecord <KeyType extends string,ValueType > = {[Key inKeyType ]:ValueType }
Here’s the built-in TypeScript type, which matches this pretty much exactly:
ts
/*** Construct a type with a set of properties K of type T*/type Record<K extends keyof any, T> = {[P in K]: T}
You may notice the keyof any
difference. As you can see below, it’s
just string | number | symbol
tsTry
letanyKey : keyof any
Use with indexed access types
Mapped types work beautifully with indexed access types, because the index can be used when defining the value type.
tsTry
typePartOfWindow = {[Key in| "document"| "navigator"| "setTimeout"]:Window [Key ]}
We couldn’t have written any equivalent of Window[Key]
using regular
index signatures. What we end up with, in a sense, is almost as if
the mapped type loops over all of the possible keys, and determines
the appropriate value type
for each key
Let’s make this a little more generalized through the use of type params.
First, we should let the caller define which keys they’d like to use. We’ll call
this type PickWindowProperties
because we get to specify which things from
Window
we’d like
tsTry
typePickWindowProperties <Keys extends keyofWindow > = {[Key inKeys ]:Window [Key ]}typePartOfWindow =PickWindowProperties <"document" | "navigator" | "setTimeout">
Let’s generalize it one step further by allowing this type to work on anything,
not just a Window
. Because this is no longer a type that exclusively
works with Window
, we’ll rename this type to PickProperties
.
tsTry
typePickProperties <ValueType ,Keys extends keyofValueType > = {[Key inKeys ]:ValueType [Key ]}typePartOfWindow =PickProperties <Window ,"document" | "navigator" | "setTimeout">
Pick
We’ve arrived at another built-in TypeScript utility type: Pick
. Our PickProperties
now matches it exactly (the names of our type params are not of consequence)
ts
/*** 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]}
tsTry
typePickProperties <ValueType ,Keys extends keyofValueType > = {[Key inKeys ]:ValueType [Key ]}
Mapping modifiers
Following our analogy of mapped types feeling like “looping over all keys”,
there are a couple of final things we can do to the properties as we
create each type: set whether the value placed there should be readonly
and/or optional
This is fairly straightforward, and you can see the use of the
?
and readonly
in the three more built-in TypeScript utility
types below.
If there’s a -
to the left of readonly
or ?
in a mapped type, that
indicates removal of this modifier instead of application of the modifier.
ts
/*** Make all properties in T optional*/type Partial<T> = {[P in keyof T]?: T[P]}/*** Make all properties in T required*/type Required<T> = {[P in keyof T]-?: T[P]}/*** Make all properties in T readonly*/type Readonly<T> = {readonly [P in keyof T]: T[P]}
There is no built-in TypeScript “utility type” for readonly
removal,
but you could implement one if you needed to (not necessarily a good idea though)
tsTry
typeNotReadonly <T > = {-readonly [P in keyofT ]:T [P ]}
Template literal types
TypeScript 4.1 brought with it template literal types.
Below you can see an example of how we take three things you could find in a painting, and four paint colors you could use.
tsTry
typeArtFeatures = "cabin" | "tree" | "sunset"typeColors =| "darkSienna"| "sapGreen"| "titaniumWhite"| "prussianBlue"
We can use the exact same syntax that one would find in an ECMAScript template literal, but in a type expression instead of a value expression, to create a new type that represents every possible combination of these art features and colors
tsTry
typeArtMethodNames = `paint_${Colors }_${ArtFeatures }`
While something like "paint_darkSienna_cabin"
could definitely be the
name of a class method in JavaScript or TypeScript, it’s more conventional
to use camelCase
instead of snake_case
TypeScript provides a few special types you can use within these template literal types
UpperCase
LowerCase
Capitalize
Uncapitalize
tsTry
typeArtMethodNames =`paint${Capitalize <Colors >}${Capitalize <ArtFeatures >}`
There we go. paintDarkSiennaCabin
is much more aligned with
what we’re used to seeing for function names.
Now, let’s bring this back into the world of Mapped Types, to perform some key mapping, where the resultant Mapped Type has different property names than the type being “iterated over” during the mapping.
Note the use of the as
keyword in the index signature
tsTry
interfaceDataState {digits : number[]names : string[]flags :Record <"darkMode" | "mobile", boolean>}typeDataSDK = {// The mapped type[K in keyofDataState as `set${Capitalize <K >}`]:(arg :DataState [K ]) => void}functionload (dataSDK :DataSDK ) {dataSDK .setDigits ([14])Object literal may only specify known properties, but 'mobil' does not exist in type 'Record<"darkMode" | "mobile", boolean>'. Did you mean to write 'mobile'?2561Object literal may only specify known properties, but 'mobil' does not exist in type 'Record<"darkMode" | "mobile", boolean>'. Did you mean to write 'mobile'?dataSDK .setFlags ({darkMode : true,: false }) mobil }
If you’ve ever written data layer code, where often there are defined
types available, and potentially you have a lot of is*
, get*
and set*
methods, you’re probably starting to see how Mapped Types have the potential
to provide rich validation across a wide range of data models.
Filtering properties out
We’ve already seen how we could filter properties out of a mapped type, if the filter condition is based on the key.
Here’s an example using Extract
and a template literal type
to filter for only those members of window.document
that begin with "query"
:
tsTry
typeDocKeys =Extract <keyofDocument , `query${string}`>typeKeyFilteredDoc = {[K inDocKeys ]:Document [K ]}
But what if we needed to filter by value? To put this another way,
what if we wanted things to be included or excluded from our mapped type
based on Document[K]
?
Our solution has to do with never
and conditional types.
Here we’re using a flawed approach, where we set the “type of the value”
to never
whenever
we want to skip it. This is going to leave us with a type that still has 100%
of the keys that Document
has, with many many values of type never
tsTry
///////////////////////////////////////////////////////////// EXAMPLE OF WHAT NOT TO DO. DO NOT FOLLOW THIS EXAMPLE /////////////////////////////////////////////////////////////typeValueFilteredDoc = {[K in keyofDocument ]:Document [K ] extends (...args : any[]) =>Element |Element []?Document [K ]: never}functionload (doc :ValueFilteredDoc ) {doc .querySelector ("input")}
Click Try and poke at this code in the TypeScript playground. While we’re kind of “blocked” from using the things we tried to omit in our mapped type, this is quite messy.
A better approach, which will get us a much cleaner result is to filter our keys first and then use those keys to build a mapped type
tsTry
// Get keys of type T whose values are assignable to type UtypeFilteredKeys <T ,U > = {[P in keyofT ]:T [P ] extendsU ?P : never}[keyofT ] &keyofT typeRelevantDocumentKeys =FilteredKeys <Document ,(...args : any[]) =>Element |Element []>typeValueFilteredDoc =Pick <Document ,RelevantDocumentKeys >functionload (doc :ValueFilteredDoc ) {doc .querySelector ("input")}