Rick Carlino

Lead software developer and co-founding member @ Farmbot, Inc.

Co-founder @ Fox.Build Makerspace, St. Charles, IL.

Reddit Twitter GitHub LinkedIn Stack Overflow Email Updates

Real World Use Case For Typescript Record Types

February 27 2017

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:

1
2
3
4
5
6
7
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:

1
2
// 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:

1
2
3
4
5
6
7
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:

1
2
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:

1
2
3
4
5
6
7
8
9
10
// 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!

1
2
3
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:

1
2
3
4
5
6
7
8
9
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.

(C) 2017 Rick Carlino