Обеспечивает ли тип индексируемых членов объекта Typescript?


292

Я хотел бы сохранить отображение строки -> строка в объекте Typescript и обеспечить, чтобы все ключи отображались на строки. Например:

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

Есть ли способ для меня, чтобы обеспечить, что значения должны быть строками (или любой другой тип ..)?

Ответы:


524
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

13
Что интересно в этом синтаксисе, так это то, что любой тип, отличный от 'string' для ключа, на самом деле неверен. Имеет смысл, учитывая, что карты JS явно обозначены строками, но это делает синтаксис несколько избыточным. Что-то вроде '{}: string' для простого указания типов значений может показаться более простым, если только они не будут добавлены каким-либо образом, чтобы разрешить автоматическое приведение ключей в действие как часть сгенерированного кода.
Арментаж

43
numberтакже допускается как тип индексации
Райан Кавано

3
Стоит отметить: компилятор обеспечивает только тип значения, а не тип ключа. Вы можете делать вещи [15] = «что угодно», и это не будет жаловаться.
amwinter

3
Нет, он устанавливает тип ключа. Вы не можете делать вещи [myObject] = 'что угодно', даже если myObject имеет хорошую реализацию toString ().
AlexG

7
@RyanCavanaugh Возможно ли (или это будет) использовать в type Keys = 'Acceptable' | 'String' | 'Keys'качестве типа индексации (ключа)?
Калеббойд

131
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'.
};

Здесь интерфейс AgeMapприменяет ключи в виде строк, а значения в виде чисел. Ключевое слово nameможет быть любым идентификатором и должно использоваться, чтобы предложить синтаксис вашего интерфейса / типа.

Вы можете использовать аналогичный синтаксис, чтобы обеспечить наличие у объекта ключа для каждой записи в типе объединения:

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

Вы, конечно, можете сделать это и универсальным типом!

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 обновление: посмотрите ответ @ dracstaxi ниже - теперь есть встроенный тип, Recordкоторый делает большую часть этого за вас.

1.2.2020 обновление: я полностью удалил готовые сопоставления интерфейсов из своего ответа. Ответ @ dracstaxi делает их совершенно неактуальными. Если вы все еще хотите их использовать, проверьте историю изменений.


1
{[ключ: номер]: T; } не является безопасным с точки зрения типов, потому что внутренне ключи объекта всегда являются строкой - см. комментарий к вопросу по @tep для более подробной информации. Например, «Выполнение x = {}; x[1] = 2;в Chrome» Object.keys(x)возвращает [«1»] и JSON.stringify(x)возвращает «{« 1 »: 2}». Угловые случаи с typeof Number(например, Infinity, NaN, 1e300, 999999999999999999999 и т. Д.) Преобразуются в строковые ключи. Также остерегайтесь других случаев угловых для строковых ключей , как x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
Робокат

@robocat Это правда, и я несколько раз редактировал, чтобы на какое-то время удалить из этого ответа числовые интерфейсы. В конечном итоге я решил сохранить их, так как TypeScript технически и частности, допускает использование чисел в качестве ключей. Сказав это, я надеюсь, что любой, кто решит использовать объекты с индексами, увидит ваш комментарий.
Сэнди Гиффорд

76

Быстрое обновление: начиная с Typescript 2.1 существует встроенный тип, Record<T, K>который действует как словарь.

Пример из документации:

// 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 Документация по Record<T, K>

Единственный недостаток я вижу в использовании над этим в {[key: T]: K}том , что вы можете кодировать полезную информацию о каком ключе используется вместо «ключа» , например , если объект был только простые ключи можно намекнуть , что вот так: {[prime: number]: yourType}.

Вот регулярное выражение, которое я написал, чтобы помочь с этими преобразованиями. Это преобразует только те случаи, когда метка является «ключом». Чтобы преобразовать другие метки, просто измените первую группу захвата:

Найти: \{\s*\[(key)\s*(+\s*:\s*(\w+)\s*\]\s*:\s*([^\}]+?)\s*;?\s*\}

Заменить: Record<$2, $3>


3
Это должно получить больше голосов - по сути, новая, нативная версия моего ответа.
Сэнди Гиффорд

Компилируется ли запись в {}?
Дуглас Гаскелл

Типы @DouglasGaskell не присутствуют в скомпилированном коде. Records (в отличие, скажем, от Javascript Maps) обеспечивают только способ принудительного применения содержимого литерала объекта. Вы не можете new Record...и const blah: Record<string, string>;будете компилировать const blah;.
Сэнди Гиффорд

Вы даже не представляете, насколько этот ответ мне помог, спасибо вам большое :)
Федерико Гранди,

Стоит отметить, что струнные союзы также работают в Records: const isBroken: Record<"hat" | "teapot" | "cup", boolean> = { hat: false, cup: false, teapot: true };
Сэнди Гиффорд

10

Ответ @Ryan Cavanaugh полностью в порядке и остается в силе. Тем не менее, стоит добавить, что начиная с Fall'16, когда мы можем утверждать, что ES6 поддерживается большинством платформ, почти всегда лучше придерживаться Map всякий раз, когда вам нужно связать некоторые данные с каким-либо ключом.

Когда мы пишем, let a: { [s: string]: string; }нам нужно помнить, что после компиляции машинописного текста нет такой вещи, как тип данных, она используется только для компиляции. И {[s: строка]: строка; } скомпилируется как раз {}.

Тем не менее, даже если вы напишите что-то вроде:

class TrickyKey  {}

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

Это просто не скомпилируется (даже если target es6вы получитеerror TS1023: An index signature parameter type must be 'string' or 'number'.

Таким образом, практически вы ограничены строкой или числом в качестве потенциального ключа, поэтому здесь нет особого смысла в принудительной проверке типа, особенно учитывая, что когда js пытается получить доступ к ключу по номеру, он преобразует его в строку.

Поэтому вполне безопасно предположить, что наилучшей практикой является использование Map, даже если ключи строковые, поэтому я бы придерживался:

let staff: Map<string, string> = new Map();

4
Не уверен, что это было возможно, когда этот ответ был опубликован, но сегодня вы можете сделать это: let dict: {[key in TrickyKey]: string} = {}- где TrickyKeyтип строкового литерала (например "Foo" | "Bar").
Роман Старков

Лично мне нравится нативный формат машинописи, но вы правы, лучше всего использовать стандарт. Это дает мне возможность задокументировать ключ «имя», который на самом деле не пригоден для использования, но я могу сделать ключ под названием что-то вроде «PatientId» :)
кодирование

Этот ответ абсолютно верен и дает очень хорошие замечания, но я не согласен с идеей, что почти всегда лучше придерживаться нативных Mapобъектов. Карты поставляются с дополнительными затратами памяти, и (что более важно) должны быть созданы вручную из любых данных, хранящихся в виде строки JSON. Они часто очень полезны, но не просто ради того, чтобы вытащить из них типы.
Сэнди Гиффорд

7

Определить интерфейс

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

Использовать ключ, чтобы быть определенным ключом интерфейса настроек

private setSettings(key: keyof Settings, value: any) {
   // Update settings key
}

3

Основываясь на ответе @ shabunc, это позволит применить либо ключ, либо значение - или оба - все, что вы хотите применить.

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

Также должно работать, используя enumвместо typeопределения.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.