Understanding TypeScript Records

April 25, 2020

👋 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 type T. 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! 🎉


Profile picture

Written by Ricardo Magalhães who works for the Internet. By day, a front-end web developer with a passion for typography and design. By night, he sleeps. Follow me on Mastodon.