👋 By the end of this article, you’ll be able to understand what Record<T, K>
means in TS and how to use it. Unless I did a really bad job, in which case you should angry tweet at me.
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: string
prop2: string
prop3: 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 string
type ProductID = string
// Defining our available types: anything out of this range will not compile
type 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: AvailabilityTypes
amount: 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.ts
type ProductID = string
type AvailabilityTypes = 'sold_out' | 'in_stock' | 'pre_order'
interface Availability {
availability: AvailabilityTypes
amount: number
}
// store.ts
const 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! 🎉