397

I have some JavaScript code that uses objects as dictionaries; for example a 'person' object will hold a some personal details keyed off the email address.

var people = {<email> : <'some personal data'>};

adding   > "people[<email>] = <data>;" 
getting  > "var data = people[<email>];" 
deleting > "delete people[<email>];"

Is it possible to describe this in Typescript? or do I have to use an Array?

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Robert Taylor
  • 4,225
  • 2
  • 17
  • 9

6 Answers6

615

In newer versions of typescript you can use:

type Customers = Record<string, Customer>

In older versions you can use:

var map: { [email: string]: Customer; } = { };
map['foo@gmail.com'] = new Customer(); // OK
map[14] = new Customer(); // Not OK, 14 is not a string
map['bar@hotmail.com'] = 'x'; // Not OK, 'x' is not a customer

You can also make an interface if you don't want to type that whole type annotation out every time:

interface StringToCustomerMap {
    [email: string]: Customer;
}

var map: StringToCustomerMap = { };
// Equivalent to first line of above
Beau Smith
  • 33,433
  • 13
  • 94
  • 101
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • 2
    That's a useful way to make sure that the compiler restricts indexes to strings. Interesting. Doesn't look like you can specify the index type to be anything other than strings or integers, but that makes sense, since it just maps to the native JS object indexes. – Ken Smith Nov 29 '12 at 18:03
  • 7
    You may know this, but there are also some potential gotchas with this approach, the big one being that there's no safe and easy way to iterate through all the members. This code, for instance, shows that `map` contains two members: (Object.prototype).something = function(){}; class Customer{} var map: { [email: string]: Customer; } = { }; map['foo@gmail.com'] = new Customer(); for (var i in map){ console.log(map[i]) } – Ken Smith Nov 30 '12 at 17:21
  • 5
    how do you remove from it? – TDaver Apr 16 '13 at 18:07
  • 26
    Another interesting approach is: interface MapStringTo { [key:string]:T; } And the declare the variable like `var map:MapStringTo = {};` – orellabac Aug 24 '13 at 00:50
  • @KenSmith The only gotcha comes if somebody changes `Object.prototype`. This technique works great. – Carl Walsh Dec 13 '13 at 05:44
  • "map[14] = new Customer(); // Not OK, 14 is not a string", doesn't work in typescript rc1, it doesn't give me a error, it just compiles. – Rob Segerink Mar 17 '14 at 11:13
  • 1
    Take note that the index constraint no longer works. [Read more.](https://github.com/Microsoft/TypeScript/issues/3150) – David Sherret May 13 '15 at 15:54
  • @TDaver The entries are added as object properties, so you can remove them using the delete keyword. – Rob Hardy Oct 02 '15 at 09:56
  • 1
    You should use the new ES6 `Map` object. https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Map – Dolan Nov 24 '16 at 16:45
  • this Map looks like a god send but its really a disaster to work with. iterating over keys or values is a nightmare, and if server sends a regular object it is not recognized as a Map. – Sonic Soul Mar 19 '19 at 22:25
  • First note is I would no longer call it Map, since that is a keyword now in ES6, I call it dictionary. Second is I got stuck for a while on iterating and then started using the Object.keys(dictionary) and then you can do any iterating fun you want, such as .map() and .filter(). – Jeremy Jun 06 '19 at 05:33
  • 1
    I prefer `[email: string]: Customer` over `Record` because the "email: string" part captures more intent about what the _meaning_ of the key is, not just its type. – nokola Oct 08 '19 at 23:37
156

In addition to using an map-like object, there has been an actual Map object for some time now, which is available in TypeScript when compiling to ES6, or when using a polyfill with the ES6 type-definitions:

let people = new Map<string, Person>();

It supports the same functionality as Object, and more, with a slightly different syntax:

// Adding an item (a key-value pair):
people.set("John", { firstName: "John", lastName: "Doe" });

// Checking for the presence of a key:
people.has("John"); // true

// Retrieving a value by a key:
people.get("John").lastName; // "Doe"

// Deleting an item by a key:
people.delete("John");

This alone has several advantages over using a map-like object, such as:

  • Support for non-string based keys, e.g. numbers or objects, neither of which are supported by Object (no, Object does not support numbers, it converts them to strings)
  • Less room for errors when not using --noImplicitAny, as a Map always has a key type and a value type, whereas an object might not have an index-signature
  • The functionality of adding/removing items (key-value pairs) is optimized for the task, unlike creating properties on an Object

Additionally, a Map object provides a more powerful and elegant API for common tasks, most of which are not available through simple Objects without hacking together helper functions (although some of these require a full ES6 iterator/iterable polyfill for ES5 targets or below):

// Iterate over Map entries:
people.forEach((person, key) => ...);

// Clear the Map:
people.clear();

// Get Map size:
people.size;

// Extract keys into array (in insertion order):
let keys = Array.from(people.keys());

// Extract values into array (in insertion order):
let values = Array.from(people.values());
John Weisz
  • 30,137
  • 13
  • 89
  • 132
  • 3
    Thats awesome! But sadly it got wrong serialized using `JSON.stringify()`, so it can be used e.g. for socket.io :( – Lion May 25 '18 at 17:23
  • @Lion -- well yes, `Map` serialization is rather funny. I, for one, perform a conversion to key-value-pair objects before serializing, and then back (e.g. object of `{ key: "John", value: { firstName: "John" } }`). – John Weisz May 25 '18 at 19:30
  • 5
    I made the mistake of using a map instead of a plain old object, and the serialization really got me. Steer clear in my opinion. – user378380 Mar 20 '19 at 22:44
  • 1
    This is beautiful. So glad you inspired me to finally dip into maps. This will pretty much replace my usual keymap/dictionary structures since it's so much easier to strongly type the keys. – Methodician Aug 27 '19 at 20:57
  • yeah maps aren't always the greatest choice, say for example you wanted to get a key but case insensitive. Not possible. – R-D Jun 11 '21 at 15:23
83

You can use templated interfaces like this:

interface Map<T> {
    [K: string]: T;
}

let dict: Map<number> = {};
dict["one"] = 1;
  • 8
    Note that this collides with the es6 Map type. Better than the other answer because the index constraint is ignored. – Old Badman Grey Jun 21 '16 at 09:15
  • 2
    I use Dictionary instead of Map to avoid confusion, and you can use the literal object notation: `let dict: Dictionary = { "one": 1, "two": 2 };` – PhiLho Jul 11 '17 at 17:44
12

You can use Record for this:

https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt

Example (A mapping between AppointmentStatus enum and some meta data):

  const iconMapping: Record<AppointmentStatus, Icon> = {
    [AppointmentStatus.Failed]: { Name: 'calendar times', Color: 'red' },
    [AppointmentStatus.Canceled]: { Name: 'calendar times outline', Color: 'red' },
    [AppointmentStatus.Confirmed]: { Name: 'calendar check outline', Color: 'green' },
    [AppointmentStatus.Requested]: { Name: 'calendar alternate outline', Color: 'orange' },
    [AppointmentStatus.None]: { Name: 'calendar outline', Color: 'blue' }
  }

Now with interface as value:

interface Icon { Name: string Color: string }

Usage:

const icon: SemanticIcon = iconMapping[appointment.Status]

Nick N.
  • 12,902
  • 7
  • 57
  • 75
  • This is very useful. Would you use a string `enum` or a `class/object` for `AppointmentStatus` - or does it matter? – Drenai Oct 27 '19 at 23:04
  • @Drenai doesn't matter, it's what you prefer – Nick N. Nov 21 '19 at 13:41
  • I figured it out myself, to use enum in a record before I have found this answer. the main benefit is that it enforces exhaustiveness check. means none of the keys from enum can be skipped. none keys not included in enum can be added. this is a dictionary with run-type check – Oleg Abrazhaev Aug 25 '20 at 17:53
11

You can also use the Record type in typescript :

export interface nameInterface { 
    propName : Record<string, otherComplexInterface> 
}
Twen
  • 302
  • 2
  • 6
8

Lodash has a simple Dictionary implementation and has good TypeScript support

Install Lodash:

npm install lodash @types/lodash --save

Import and usage:

import { Dictionary } from "lodash";
let properties : Dictionary<string> = {
    "key": "value"        
}
console.log(properties["key"])
phil
  • 3,538
  • 2
  • 24
  • 24