The dark side of Record in TypeScript

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 inst…


This content originally appeared on DEV Community and was authored by Taras Zubyk

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.


This content originally appeared on DEV Community and was authored by Taras Zubyk


Print Share Comment Cite Upload Translate Updates
APA

Taras Zubyk | Sciencx (2022-07-03T16:58:09+00:00) The dark side of Record in TypeScript. Retrieved from https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/

MLA
" » The dark side of Record in TypeScript." Taras Zubyk | Sciencx - Sunday July 3, 2022, https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/
HARVARD
Taras Zubyk | Sciencx Sunday July 3, 2022 » The dark side of Record in TypeScript., viewed ,<https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/>
VANCOUVER
Taras Zubyk | Sciencx - » The dark side of Record in TypeScript. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/
CHICAGO
" » The dark side of Record in TypeScript." Taras Zubyk | Sciencx - Accessed . https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/
IEEE
" » The dark side of Record in TypeScript." Taras Zubyk | Sciencx [Online]. Available: https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/. [Accessed: ]
rf:citation
» The dark side of Record in TypeScript | Taras Zubyk | Sciencx | https://www.scien.cx/2022/07/03/the-dark-side-of-record-in-typescript/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.