Record
is a global utility type provided by TypeScript that constructs an object type whose keys are TKey
and values are TValue
.
Record<TKey, TValue>
Record
is handy when you need to map the properties of a type to another type. For instance, below we map ColorName
to the object of type Color
.
type ColorName = 'blue' | 'yellow' | 'white';
interface Color
hex: string;
rgb: r: number; g: number; b: number ;
const colors: Record<ColorName, Color> =
blue: hex: '#0057b7', rgb: r: 0, g: 87, b: 183 ,
yellow: hex: '#ffd700', rgb: r: 255, g: 215, b: 0 ,
white: hex: '#ffffff', rgb: r: 255, g: 255, b: 255
;
Before unveiling the dark side of Record
let’s first understand so-called exhaustive types.
Exhaustive vs. Non-Exhaustive Types
A type is exhaustive if it has a finite number of possible values. For instance, a union type of string literals or an enum are exhaustive types:
type ColorName = 'blue' | 'yellow' | 'white';
enum Priority
low = 'low',
normal = 'normal',
high = 'high',
On the contrary, a type is non-exhaustive if it has an infinite number of possible values. For example, string
or number
.
Often times developers mistakenly think of Record
as a normal JS object. In other words, they use it with both exhaustive and non-exhaustive keys. This eventually leads to runtime errors like “Cannot read properties of undefined”.
For instance, let’s use string
(non-exhaustive) instead of ColorName
(exhaustive) as a key type for our Record
object to see what it changes.
// We tell TypeScript that values of `colors` will be of type `Color`.
const colors: Record<string, Color> = ;
// But in practice they can be `undefined` too.
const blue = colors["blue"];
// There is no TypeScript error below but you'll get a runtime error if `colors` doesn't contain a value for the key "blue".
const hex = blue.hex;
// ^ Uncaught TypeError: Cannot read properties of undefined
It’s very easy to make such a mistake and, based on my experience, it’s a pretty common one. Especially if you or your teammates are new to TypeScript.
I believe that using Record
with non-exhaustive keys should be frowned upon and discouraged.
Let’s look at some type-safe ways to create objects with a non-exhaustive key.
Map
Map is a native JS data structure that is supported across all major browsers. It can be used to create a dictionary with non-exhaustive keys as its get
method will always return an optional value.
const colors: Map<string, Color> = new Map<string, Color>();
const blue = colors.get('blue');
// We get the following TypeScript error if we try to access a value from the `colors` map.
const hex = blue.hex;
// ^^^^ Object is possibly 'undefined'.(2532)
PartialRecord
Another alternative is a custom type that will enforce optionality for object values. Let’s call it PartialRecord
.
type PartialRecord<TKey extends PropertyKey, TValue> =
[key in TKey]?: TValue;
It can be used interchangeably with the Record
type when you work with non-exhaustive keys.
const colors: PartialRecord<string, Color> = ;
const blue = colors['blue'];
// Similar to `Map`, we also get a TypeScript error if we try to access a value from the `colors` object.
const hex = blue.hex;
// ^^^^ Object is possibly 'undefined'.(2532)
Summary
Record
works great for situations when you want to map an exhaustive type to another type. If we didn’t explicitly enumerate all values of ColorName
, TypeScript would immediately tell us what we need to fix:
type ColorName = 'blue' | 'yellow' | 'white';
const colors: Record<ColorName, Color> =
// ^^^^^^ Property 'blue' is missing in type ...
yellow: hex: '#ffd700', rgb: r: 255, g: 215, b: 0 ,
white: hex: '#ffffff', rgb: r: 255, g: 255, b: 255
;
However, when you work with non-exhaustive keys you’ll be better off with Map
or a custom type like PartialRecord
. I personally prefer PartialRecord
because of its neat initialisation and resemblance of Record
:
// Initialisation using `Map`.
const colorsMap: Map<string, Color> = new Map<string, Color>([
["yellow", hex: "#ffd700", rgb: r: 255, g: 215, b: 0 ],
]);
// Initialisation using `PartialRecord`.
const colorsObject: PartialRecord<string, Color> = {
yellow: hex: "#ffd700", rgb: r: 255, g: 215, b: 0
Let me know in the comments if you’ve experienced similar issues with Record
and what approach you took to prevent them in the future.