How to Type a Keyed Collection in TypeScript

last updated: Oct 23rd, 2020

I stumbled across a new type definition today -- at least it was new to me -- and I was so excited because I'd been using a less-than-perfect alternative for so long.

Before I go on, I should clarify what I mean by a keyed collection. Different languages call it by different names. In JavaScript/TypeScript it's just an object since object is so versatile. (Okay, technically there is a Map in JavaScript too but it has a different interface). In C# it's called a Dictionary.

Some people might call it a lookup or a map.

Anyhow, when I use an object in TypeScript to store info keyed by other info then I've typically called it a map or a dictionary.

First Principles

For the longest time (and in much older versions of TypeScript) I would create type definitions like this.

type ThingId = string;type Thing = {    id: ThingId;    name: string;    width: number;    length: number;    height: number;};type ThingMap = {//    [id: ThingId]: Thing; <-- Compiler error: index type must be string or number. Ugh.    [id: string]: Thing;};type Foo = {    things: ThingMap;    // ... other things};

I hated that because it was somewhat verbose to type a dictionary of objects. But also the fact that TypeScript wouldn't let me type the key correctly grated my nerves immensely.

The perfectionist in me was not happy.

Lodash

Eventually I noticed that lodash has a Dictionary<> type! Though, upon closer inspection, it's not ideal because it still didn't let me type the key correctly. But it addressed the issue of verbosity.

import { Dictionary } from 'lodash';// ...type Foo = {    things: Dictionary<Thing>;    // ... other things};

So I've been using Dictionary<>... I don't know... for the better part of two years maybe. And I dislike it but it's still better than before.

Record Type

Just today I noticed that TypeScript now includes a Record<Key, Type> type. This is exactly what I've wanted for so long.

type Foo = {    things: Record<ThingId, Thing>;    // ... other things};

And it's smart about the type of things that Key can be. For instance, if you had a fixed set of keys and the key type was a union of those things:

type Fruit = 'apple' | 'banana' | 'cantaloupe';const fruitSalad: Record<Fruit, string> = {    apple: 'yum',    banana: 'yum',    cantaloupe: 'meh',};console.log(fruitSalad.apple); // Works!console.log(fruitSalad['banana']); // Works!console.log(fruitSalad.dragonfruit); // Fails as expected

That last line gives a compiler error:

Element implicitly has an 'any' type because expression of type '"dragonfruit"'can't be used to index type 'Record<Fruit, string>'.  Property 'dragonfruit' does not exist on type 'Record<Fruit, string>'. ts(7053)

Official Documentation

Here is the official TypeScript documentation on the Record type and a bunch of other super useful type definitions.

Level up Your React + Redux + TypeScript

with articles, tutorials, sample code, and Q&A.