ChatGPT解决这个技术问题 Extra ChatGPT

Enforcing the type of the indexed members of a Typescript object?

I would like to store a mapping of string -> string in a Typescript object, and enforce that all of the keys 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..)?


Z
ZephDavies
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

number is also allowed as an indexing type
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.
No, it does enforce the key type. You can't do stuff[myObject] = 'whatever' even if myObject has a nice toString() implementation.
@RyanCavanaugh Is it possible (or will it be) to use a type Keys = 'Acceptable' | 'String' | 'Keys' as an indexing (key) type?
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
S
Sandy Gifford
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,
};

10.10.2018 update: Check out @dracstaxi's answer below - there's now a built-in type Record which does most 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.


{ [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 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.
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 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!
d
dracstaxi

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>


This should get more upvotes - it's essentially the newer, native version of my answer.
Does record compile into a {} ?
@DouglasGaskell types don't have any presence in compiled code. Records (unlike, say, Javascript Maps) only provide a way to enforce the contents of an object literal. You cannot new Record... and const blah: Record<string, string>; will compile to const blah;.
You can't even imagine how much this answer helped me, thank you soo much :)
Worth mentioning that string unions work in Records as well: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
A
AmerllicA

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,
};

I
Ivan Pesochenko

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

L
Lasithds

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
}

s
shabunc

@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();

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").
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" :)
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.
R
Roy Art

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.


x
x-magix
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


Is there a way to get the tmpParams[customkey] to have the appropriate value? Not just any ?
A
Ahmet Emrebas
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!');