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
Kof 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
availabilitytypes - Type the
amountas 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! 🎉