Mapped types were added to Typescript in version 2.1. They are one of the best (but sometimes overlooked) features of the language. Once you master mapped types, you can write code that is easier to understand, easier to refactor and safer at runtime.
The addition of mapped types in version 2.1 brought a new generic Record
type,
which is what I will focus on in this article.
Let’s consider some pre 2.1 code before taking a look. Suppose we’re building a
data structure for users to store arbitrary settings. It stores preferences like
SHOE_SIZE
(number), PET_NAME
(string) or perhaps even LEFT_HANDED
(boolean).
Here’s a simple solution that meets the requirements:
type UserPref = {
[pref: string]: any;
};
let prefs: UserPref = {
"PET_NAME": "Fido",
};
This works fine for a while, but after some time, we realized that dictionary types were slowly causing spelling and capitalization errors to sneak into our code:
// Opps, we were supposed to write the key name `LIKE_THIS`
prefs["petName"] = "This is actually a typo.";
Additionally, our project manager let’s us know that in reality, the only
settings we actually want to look for are PET_NAME
, SEATING_PREFERENCE
and
AGE
.
How can we prevent spelling errors and facilitate refactoring later? It’s easy to remember the names of three settings today, but as the amount of preferences grow, it becomes harder to keep track of the possibilities. No matter how skilled we are at global find and replace, spelling errors always have a way of getting into source control. Additionally, when new developers join the team (and the possible preferences grow into the hundreds), it will be difficult for them to learn all the possibilities.
To minimize errors, we try this:
type PrefName = "PET_NAME" | "SEATING_PREFERENCE" | "AGE";
type UserPref = {
// WON'T WORK!!
[pref: PrefName]: any;
// Wrong! ^
};
The above code is not valid Typescript, unfortunately. we get the following response from the type checker:
An index signature parameter type must be 'string' or 'number'.
(parameter) pref: PrefName
It would be ideal to have the flexibility of a dictionary, but restrict the possible keys to a subset of values. This would make spelling errors preventable at compile time and also leave documentation in the form of type annotations.
This is now possible thanks to record types! We still need to make some changes to our code, though. After studying the Typescript handbook, we arrive at the following solution:
// This will serve as a form of implicit documentation later on.
type PrefName = "PET_NAME" | "SEATING_PREFERENCE" | "AGE";
// This will act as a "restricted dictionary".
type UserPref = Record<PrefName, any>;
// ALMOST works (but we forgot one thing):
let prefs: UserPref = {
"PET_NAME": "fido",
};
But now we have a new error on our hands!
Type '{ "PET_NAME": string; }' is not assignable to type 'Record<PrefName, any>'.
Property 'SEATING_PREFERENCE' is missing in type '{ "PET_NAME": string; }'.
let prefs: Record<PrefName, any>
Typescript wants us to define all of the properties in the Record
type all at
once.
We don’t have all of that information at the moment, as we’re trying to build
this object incrementally. Moreover, being forced to type every single
preference every time we want to pass a UserPref
variable will become tedious
when the amount of settings grow.
This is where Partial
types come into play. The Partial
generic type (also
introduced in TSC 2.1) lets us define an interface that is a subset of an
existing type:
type PrefName = "PET_NAME" | "SEATING_PREFERENCE" | "AGE";
// Make it a `Partial` type also:
type UserPref = Partial<Record<PrefName, any>>;
let prefs: UserPref = {
// Typescript no longer cares that "SEATING_PREFERENCE" and "AGE" are missing.
"PET_NAME": "fido",
};
Partial
is a great tool for forms and other data structures that are slowly
built up over time. It’s also useful for methods like React’s setState({})
and
Backbone’s model.set({})
where you’re expected to pass in only a portion of a
valid object. In our case, it allows us to store a subset of user preferences.
Record
types are a great tool to have in your toolbox when writing Typescript
applications. They add type safety and flexibility. Mapped types also make it
easier to refactor code later by letting the type checker keep track of key
names. Once you’ve mastered Partial
and Record
types, consider taking a look
at some of the other new features such as Pick
and Readonly
.