Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

typescript

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..)?

like image 771
Armentage Avatar asked Nov 09 '12 20:11

Armentage


People also ask

How do you define type of key in an object?

Use the keyof typeof syntax to create a type from an object's keys, e.g. type Keys = keyof typeof person . The keyof typeof syntax returns a type that represents all of the object's keys as strings. index.ts.

What is index type in TypeScript?

The indexing type is itself a type, so we can use unions, keyof , or other types entirely: type I1 = Person ["age" | "name"]; type I1 = string | number. type I2 = Person [keyof Person ]; type I2 = string | number | boolean.

How do you define object of objects type in TypeScript?

To define an object of objects type in TypeScript, we can use index signatures with the type set to the type for the value object. const data: { [name: string]: DataModel } = { //... }; to create a data variable of type { [name: string]: DataModel } where DataModel is an object type.

How do you access the properties of objects in TypeScript?

To dynamically access an object's property: Use keyof typeof obj as the type of the dynamic key, e.g. type ObjectKey = keyof typeof obj; . Use bracket notation to access the object's property, e.g. obj[myVar] .


4 Answers

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
like image 141
Ryan Cavanaugh Avatar answered Oct 12 '22 10:10

Ryan Cavanaugh


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.

like image 26
Sandy Gifford Avatar answered Oct 12 '22 11:10

Sandy Gifford


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>

like image 194
dracstaxi Avatar answered Oct 12 '22 11:10

dracstaxi


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,
};
like image 27
AmerllicA Avatar answered Oct 12 '22 11:10

AmerllicA