673

I would like to store a mapping of string -> string in a Typescript object, and enforce that all of the values map to strings. For example:

var stuff = {};
stuff["a"] = "foo";   // okay
stuff["b"] = "bar";   // okay
stuff["c"] = false;   // ERROR!  bool != string

Is there a way for me to enforce that the values must be strings (or whatever type..)?

Elias Zamaria
  • 96,623
  • 33
  • 114
  • 148
Armentage
  • 12,065
  • 8
  • 33
  • 33

10 Answers10

1092
var stuff: { [key: string]: string; } = {};
stuff['a'] = ''; // ok
stuff['a'] = 4;  // error

// ... or, if you're using this a lot and don't want to type so much ...
interface StringMap { [key: string]: string; }
var stuff2: StringMap = { };
// same as above
ZephDavies
  • 3,964
  • 2
  • 14
  • 19
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • 70
    `number` is also allowed as an indexing type – Ryan Cavanaugh Nov 12 '12 at 17:15
  • Try putting null and it will tell: An index expression argument must be of type 'string', 'number', or 'any'. – Christophe Roussy Jan 27 '15 at 17:09
  • 5
    worth noting: the compiler is only enforcing the value type, not the key type. you can do stuff[15] = 'whatever' and it won't complain. – amwinter Mar 09 '15 at 19:15
  • 5
    No, it does enforce the key type. You can't do stuff[myObject] = 'whatever' even if myObject has a nice toString() implementation. – AlexG Apr 06 '16 at 08:01
  • 2
    is it wise to call it a StringMap? Not really a native Map here – SuperUberDuper Mar 17 '17 at 14:47
  • Well, the native Map type is called `Map`, so some other type must be something else. You can call it whatever you like, of course. – Ryan Cavanaugh Mar 17 '17 at 15:58
  • 1
    Could you elaborate more about this syntax ? Thank you. – Gilad Aug 04 '17 at 08:20
  • Not sure if using the `[key:string]` helps that much. Here I use a number as a key and the compiler doesn't complain: http://www.typescriptlang.org/play/#src=interface%20ShoppingCartState%20%7B%0D%0A%20%20%5Bkey%3A%20string%5D%3A%20number%3B%20%0D%0A%7D%0D%0A%0D%0Aclass%20Foo%7B%0D%0A%0D%0A%20%20%20%20constructor%20(bar%3A%20ShoppingCartState)%7B%7D%0D%0A%7D%0D%0A%0D%0Alet%20cart%20%3D%20%7B%0D%0A%20%20%20%20%22a%22%3A%201%2C%0D%0A%20%20%20%202%3A%202%0D%0A%7D%3B%0D%0A%0D%0A%0D%0Alet%20myFoo%20%3D%20new%20Foo(cart)%3B – Yakov Fain Sep 21 '17 at 12:31
  • 1
    is there any official doc about this syntax? – Leon Dec 27 '17 at 07:01
  • 9
    Be careful with `{ number: string }`, because even though this may enforce the type of the index upon assignment, the object still stores the key as a `string` internally. This can actually confuse TypeScript and break type safety. For example, if you try to convert a `{ number: string }` to a `{ string: number }` by swapping keys with values, you actually end up with a `{ string: string }` yet TypeScript doesn't throw any warnings/errors – tep Mar 13 '18 at 17:41
  • Thank you for the added `interface` example! It's much simpler and clearer than the TS docs example, which has the extra complexity of being in a function. – Mike B Feb 07 '19 at 01:54
  • You can use a subset of string as a key type, such as a string enum. – rjh Sep 17 '19 at 11:51
  • @calebboyd For anyone who has your same question, it sounds like you want the [`Record` utility type](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type), which lets you do stuff like `const cats: Record<'Miffy'|'Boris'|'Mordred', CatInfo> = { ... }`, which creates an object that only allows those three strings as keys. – Ken Bellows Jan 07 '22 at 16:09
  • Is there any chance to mix value types forcing one parameter to `string` and the rest to `number` like this: `export type BarChartData = { x: string; [bar: string]: number; };` ??? – elnezah Nov 04 '22 at 13:26
303
interface AgeMap {
    [name: string]: number
}

const friendsAges: AgeMap = {
    "Sandy": 34,
    "Joe": 28,
    "Sarah": 30,
    "Michelle": "fifty", // ERROR! Type 'string' is not assignable to type 'number'.
};

Here, the interface AgeMap enforces keys as strings, and values as numbers. The keyword name can be any identifier and should be used to suggest the syntax of your interface/type.

You can use a similar syntax to enforce that an object has a key for every entry in a union type:

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

type ChoresMap = { [DAY in DayOfTheWeek]: string };

const chores: ChoresMap = { // ERROR! Property 'saturday' is missing in type '...'
    "sunday": "do the dishes",
    "monday": "walk the dog",
    "tuesday": "water the plants",
    "wednesday": "take out the trash",
    "thursday": "clean your room",
    "friday": "mow the lawn",
};

You can, of course, make this a generic type as well!

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

type DayOfTheWeekMap<T> = { [DAY in DayOfTheWeek]: T };

const chores: DayOfTheWeekMap<string> = {
    "sunday": "do the dishes",
    "monday": "walk the dog",
    "tuesday": "water the plants",
    "wednesday": "take out the trash",
    "thursday": "clean your room",
    "friday": "mow the lawn",
    "saturday": "relax",
};

const workDays: DayOfTheWeekMap<boolean> = {
    "sunday": false,
    "monday": true,
    "tuesday": true,
    "wednesday": true,
    "thursday": true,
    "friday": true,
    "saturday": false,
};

Finally, you can use the indexing identifier as a type in your indexed type:

type DayOfTheWeek = "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday";

const weekdayMeta: { [DAY in DayOfTheWeek]: { day: DAY; score: number } } = {
  sunday: { day: "sunday", score: 3 },
  monday: { day: "monday", score: 4 },
  tuesday: { day: "tuesday", score: 2 },
  wednesday: { day: "wednesday", score: 0 },
  thursday: { day: "thursday", score: 5 },
  friday: { day: "friday", score: 6 },
  saturday: { day: "sunday", score: 1 }, // ERROR: Type '"sunday"' is not assignable to type '"saturday"'.
};

10.10.2018 update: Check out @dracstaxi's answer below - there's now a built-in type Record which does some of this for you.

1.2.2020 update: I've entirely removed the pre-made mapping interfaces from my answer. @dracstaxi's answer makes them totally irrelevant. If you'd still like to use them, check the edit history.

Sandy Gifford
  • 7,219
  • 3
  • 35
  • 65
  • 3
    { [key: number]: T; } is not typesafe because internally the keys of an object are *always* a string -- see comment on question by @tep for more details. e.g. Running `x = {}; x[1] = 2;` in Chrome then `Object.keys(x)` returns ["1"] and `JSON.stringify(x)` returns '{"1":2}'. Corner cases with typeof `Number` (e.g. Infinity, NaN, 1e300, 999999999999999999999 etc) get converted to string keys. Also beware of other corner cases for string keys like `x[''] = 'empty string';`, `x['000'] = 'threezeros';` `x[undefined] = 'foo'`. – robocat Feb 04 '19 at 23:54
  • @robocat This is true, and I've gone back and forth on editing to remove the number keyed interfaces from this answer for a while. Ultimately I've decided to keep them since TypeScript _technically_ and _specifically_ allows numbers-as-keys. Having said that, I hope that anyone who decides to use objects indexed with numbers sees your comment. – Sandy Gifford Aug 26 '19 at 16:07
  • Would it be fair to say this could be improved like so: `{ [name: string]: [age: number] }` to include the hint that the number value is an age? @SandyGifford – Fasani Sep 26 '21 at 09:12
  • @Fasani unfortunately not - the type you just defined would be an object with strings for keys, and a tuple with a single number in it for values. You CAN however use that syntax to hint what the values in a tuple are for, though! – Sandy Gifford Oct 06 '21 at 20:41
  • @Fasani see here: https://www.typescriptlang.org/play?#code/C4TwDgpgBAyg9gWwgFXNAvFA3gbQNYQgBcUAzsAE4CWAdgOYC6JOAbgIYA2ArhCTVwgBGECgwC+AbgBQUgMZwa5KGxLwkqSFExYoAIgAWEDhzi7mARgZRJUoA – Sandy Gifford Oct 06 '21 at 20:42
  • "Here, the interface AgeMap enforces keys as strings, and values as numbers" It actually does not seem to enforce key being a string. – user3761308 Dec 14 '22 at 14:24
  • @user3761308 Enforcing a key type of string is the widest typescript will allow. Since all keys are stored as strings, and any valid key (primitives) can be expressed as a string, you can use any value there and it will automatically be converted to a string by your Javascript engine. – Sandy Gifford Dec 14 '22 at 18:08
217

A quick update: since Typescript 2.1 there is a built in type Record<T, K> that acts like a dictionary.

In this case you could declare stuff like so:

var stuff: Record<string, any> = {};

You could also limit/specify potential keys by unioning literal types:

var stuff: Record<'a'|'b'|'c', string|boolean> = {};

Here's a more generic example using the record type from the docs:

// For every properties K of type T, transform it to U
function mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>

const names = { foo: "hello", bar: "world", baz: "bye" };
const lengths = mapObject(names, s => s.length);  // { foo: number, bar: number, baz: number }

TypeScript 2.1 Documentation on Record<T, K>

The only disadvantage I see to using this over {[key: T]: K} is that you can encode useful info on what sort of key you are using in place of "key" e.g. if your object only had prime keys you could hint at that like so: {[prime: number]: yourType}.

Here's a regex I wrote to help with these conversions. This will only convert cases where the label is "key". To convert other labels simply change the first capturing group:

Find: \{\s*\[(key)\s*(+\s*:\s*(\w+)\s*\]\s*:\s*([^\}]+?)\s*;?\s*\}

Replace: Record<$2, $3>

dracstaxi
  • 2,417
  • 1
  • 13
  • 12
  • Does record compile into a `{}` ? – Douglas Gaskell Mar 28 '19 at 06:54
  • @DouglasGaskell types don't have any presence in compiled code. `Record`s (unlike, say, Javascript [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)s) only provide a way to enforce the contents of an object literal. You cannot `new Record...` and `const blah: Record;` will compile to `const blah;`. – Sandy Gifford Jul 11 '19 at 18:58
  • You can't even imagine how much this answer helped me, thank you soo much :) – Federico Grandi Jul 25 '19 at 20:10
  • 2
    Worth mentioning that string unions work in `Record`s as well: `const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };` – Sandy Gifford Jan 10 '20 at 18:39
  • Actually, whether you should use Record or not, see [this](https://stackoverflow.com/a/54100359/9173778) question and answer – Dror Bar May 16 '22 at 07:05
  • That answer reads like an opinion to me. They point out in their response that using Record is equivalent to using the object key syntax. IMO Record is more expressive, but that's just an opinion – dracstaxi Jun 02 '22 at 14:44
  • But, with Record we are losing intellisense assistence: https://stackoverflow.com/questions/68246188/why-does-annotating-this-object-with-a-record-type-remove-intellisense (worth mentioning, imo) – agoldev Oct 20 '22 at 17:17
37

Actually there is a built-in utility Record:

    const record: Record<string, string> = {};
    record['a'] = 'b';
    record[1] = 'c'; // leads to typescript error
    record['d'] = 1; // leads to typescript error
Ivan Pesochenko
  • 585
  • 4
  • 11
36

You can pass a name to the unknown key and then write your types:

type StuffBody = {
  [key: string]: string;
};

Now you can use it in your type checking:

let stuff: StuffBody = {};

But for FlowType there is no need to have name:

type StuffBody = {
  [string]: string,
};
AmerllicA
  • 29,059
  • 15
  • 130
  • 154
13

@Ryan Cavanaugh's answer is totally ok and still valid. Still it worth to add that as of Fall'16 when we can claim that ES6 is supported by the majority of platforms it almost always better to stick to Map whenever you need associate some data with some key.

When we write let a: { [s: string]: string; } we need to remember that after typescript compiled there's not such thing like type data, it's only used for compiling. And { [s: string]: string; } will compile to just {}.

That said, even if you'll write something like:

class TrickyKey  {}

let dict: {[key:TrickyKey]: string} = {}

This just won't compile (even for target es6, you'll get error TS1023: An index signature parameter type must be 'string' or 'number'.

So practically you are limited with string or number as potential key so there's not that much of a sense of enforcing type check here, especially keeping in mind that when js tries to access key by number it converts it to string.

So it is quite safe to assume that best practice is to use Map even if keys are string, so I'd stick with:

let staff: Map<string, string> = new Map();
shabunc
  • 23,119
  • 19
  • 77
  • 102
  • 6
    Not sure if this was possible when this answer was posted, but today you can do this: `let dict: {[key in TrickyKey]: string} = {}` - where `TrickyKey` is a string literal type (eg `"Foo" | "Bar"`). – Roman Starkov Nov 27 '17 at 14:09
  • Personally I like the native typescript format but you're right its best to use the standard. It gives me a way to document the key "name" which isn't really usable but I can make the key called something like "patientId" :) – coding Jan 02 '18 at 17:14
  • This answer is absolutely valid, and makes very good points, but I'd disagree with the idea that it's almost always better to stick to native `Map` objects. Maps come with additional memory overhead, and (more importantly) need to be manually instantiated from any data stored as a JSON string. They are often very useful, but not purely for the sake of getting types out of them. – Sandy Gifford Jul 29 '19 at 15:27
13

Define interface

interface Settings {
  lang: 'en' | 'da';
  welcome: boolean;
}

Enforce key to be a specific key of Settings interface

private setSettings(key: keyof Settings, value: any) {
   // Update settings key
}
Lasithds
  • 2,161
  • 25
  • 39
4

Building on @shabunc's answer, this would allow enforcing either the key or the value — or both — to be anything you want to enforce.

type IdentifierKeys = 'my.valid.key.1' | 'my.valid.key.2';
type IdentifierValues = 'my.valid.value.1' | 'my.valid.value.2';

let stuff = new Map<IdentifierKeys, IdentifierValues>();

Should also work using enum instead of a type definition.

Roy Art
  • 578
  • 5
  • 10
2
interface AccountSelectParams {
  ...
}
const params = { ... };

const tmpParams: { [key in keyof AccountSelectParams]: any } | undefined = {};
  for (const key of Object.keys(params)) {
    const customKey = (key as keyof typeof params);
    if (key in params && params[customKey] && !this.state[customKey]) {
      tmpParams[customKey] = params[customKey];
    }
  }

please commented if you get the idea of this concept

x-magix
  • 2,623
  • 15
  • 19
-2
type KeyOf<T> = keyof T;

class SomeClass<T, R> {
  onlyTFieldsAllowed = new Map<KeyOf<T>, R>();
}

class A {
  myField = 'myField';
}

const some = new SomeClass<A, any>();

some.onlyTFieldsAllowed.set('myField', 'WORKS');
some.onlyTFieldsAllowed.set('noneField', 'Not Allowed!');
Ahmet Emrebas
  • 566
  • 6
  • 10