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.