Understanding TypeScript Records (ricardofilipe.com)
A definition of Record
Typescript 2.1 introduced the Record type, and the official documentation defines it as:
Constructs a type with a set of properties
K
of typeT
. This utility can be used to map the properties of a type to another type.
Its definition shows how it works internally, but it can a little scary to newcomers to the language:
type Record<K extends string, T> = {[P in K]: T}
But let’s start with a practical example. Consider the following JS object:
const object = {prop1: 'value',prop2: 'value2',prop3: 'value3',}
If we know the types that both keys and values of that object will receive, typing it with a Record
can be extremelly useful. A Record<K, T>
is an object type whose property keys are K
and whose property values are T
.
One post on StackOverflow that initially helped me understand what Record
did was this post, in which it’s clear to see that the following type definition:
type PropResponse = Record<'prop1' | 'prop2' | 'prop3', string>
Is pretty much the same as writing this, which you’re probably already familiar with as a normal type
definition:
type PropResponse = {prop1: stringprop2: stringprop3: string}
Let’s go back to our object we want to type. We know that it has 3 keys, prop1
, prop2
and prop3
, and that each of them has the value of a string. We can use the previous PropResponse
to type it, like so:
type PropResponse = Record<'prop1' | 'prop2' | 'prop3', string>const object: PropResponse = {prop1: 'value',prop2: 'value2',prop3: 'value3',}
Notice that if we change any of the values to a boolean
, TypeScript will not compile:
const object: PropResponse = {prop1: 'value',prop2: 'value2',prop3: true, // Type 'true' is not assignable to type 'string'}
Of course, very often an object is a mixed bag of types, where you’ll get strings, numbers, booleans and so on. Record
still works in these cases, because it accepts a type
as one of its values. Let’s look at a more complex example.
The classic Store example
Let’s switch to the classic Store example, with real life data. We’d like to type the in-store availability of products, grouped by ID. Each ID has an object as a value, with availability
typed as a string and the amount
available for each product.
const store = {'0d3d8fhd': { availability: 'in_stock', amount: 23 },'0ea43bed': { availability: 'sold_out', amount: 0 },'6ea7fa3c': { availability: 'sold_out', amount: 0 },}
We want to do a few things to type this correctly. We must:
- Type the key as the product ID, as a string
- Type the value with a range of
availability
types - Type the
amount
as a number
// Our product ID will be a stringtype ProductID = string// Defining our available types: anything out of this range will not compiletype AvailabilityTypes = 'sold_out' | 'in_stock' | 'pre_order'
We can also define the Availability
as a type itself, containing a value which will be one of the AvailabilityTypes
and contain the amount
as a number:
interface Availability {availability: AvailabilityTypesamount: number}
💡 Aside: note that we could have also inlined our stock strings instead of creating a new type entirely. The following would have also worked:
interface Availability {availability: 'sold_out' | 'in_stock' | 'pre_order'amount: number}
💡
And we put it all together in a Record
type, where the first argument is for our key (ProductID
) and the second is for its value (Availability
). That leaves us with Record<ProductID, Availability>
and we use it like so:
const store: Record<ProductID, Availability> = {'0d3d8fhd': { availability: 'in_stock', amount: 23 },'0ea43bed': { availability: 'sold_out', amount: 0 },'6ea7fa3c': { availability: 'sold_out', amount: 0 },}
Here’s the full typing for this example:
// types.tstype ProductID = stringtype AvailabilityTypes = 'sold_out' | 'in_stock' | 'pre_order'interface Availability {availability: AvailabilityTypesamount: number}// store.tsconst store: Record<ProductID, Availability> = {'0d3d8fhd': { availability: 'in_stock', amount: 23 },'0ea43bed': { availability: 'sold_out', amount: 0 },'6ea7fa3c': { availability: 'sold_out', amount: 0 },}
(you can play with the playground here)
There are other ways to go about and type this object of course, but Record
itself proves to be a useful abstraction that will check keys and value types for us.
Check out the official documentation for more examples and I’ll soon be back for more starter guides on TS! 🎉