7

"Как задать тип для индексируемых членов объекта в TypeScript?"

31

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

var stuff = {};
stuff["a"] = "foo";   // все в порядке
stuff["b"] = "bar";   // все в порядке
stuff["c"] = false;   // ОШИБКА! bool != string

Существует ли способ заставить TypeScript проверять, чтобы значения были строками (или любым другим типом)?

5 ответ(ов)

11

Ваш вопрос касается использования объекта в TypeScript с индексной сигнатурой. Вот перевод вашего кода с объяснением:

var stuff: { [key: string]: string; } = {};
stuff['a'] = ''; // все в порядке
stuff['a'] = 4;  // ошибка

// ... или, если вы используете это часто и не хотите много писать ...
interface StringMap { [key: string]: string; }
var stuff2: StringMap = { };
// то же самое, как и выше

В данном примере вы создаете объект stuff, который имеет индексную сигнатуру, указывающую, что ключи должны быть строками, а соответствующие значения также должны быть строками. При попытке присвоить числовое значение 4 возникнет ошибка, поскольку оно не соответствует типу string.

Если вы используете подобные структуры данных достаточно часто, вы можете определить интерфейс, как в примере с StringMap. Это сделает код более читаемым и уменьшит количество вводимых символов, не теряя при этом строгости типов.

3

Интерфейс AgeMap определяет структуру объекта, в которой ключами являются строки, а значениями — числа. В приведённом примере:

interface AgeMap {
    [name: string]: number
}

const friendsAges: AgeMap = {
    "Sandy": 34,
    "Joe": 28,
    "Sarah": 30,
    "Michelle": "fifty", // ОШИБКА! Тип 'string' не может быть присвоен типу 'number'.
};

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

Также можно использовать аналогичный синтаксис, чтобы создать объект с ключами для каждого элемента из объединённого типа. Например:

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

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

const chores: ChoresMap = { // ОШИБКА! Свойство 'saturday' отсутствует в типе '...'
    "sunday": "помыть посуду",
    "monday": "выгулять собаку",
    "tuesday": "полить растения",
    "wednesday": "вынести мусор",
    "thursday": "убрать в комнате",
    "friday": "покосить траву",
};

Данный тип также можно сделать универсальным:

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

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

const chores: DayOfTheWeekMap<string> = {
    "sunday": "помыть посуду",
    "monday": "выгулять собаку",
    "tuesday": "полить растения",
    "wednesday": "вынести мусор",
    "thursday": "убрать в комнате",
    "friday": "покосить траву",
    "saturday": "отдохнуть",
};

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

Наконец, вы можете использовать идентификаторы индекса в качестве типа в вашем индексированном типе:

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 }, // ОШИБКА: Тип '"sunday"' не может быть присвоен типу '"saturday"'.
};

Обновление 10.10.2018: Посмотрите ответ @dracstaxi ниже — теперь есть встроенный тип Record, который делает часть этого за вас.

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

0

Вы можете передать имя неизвестному ключу и затем указать свои типы:

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

Теперь вы можете использовать это в проверке типов:

let stuff: StuffBody = {};

Однако для FlowType нет необходимости указывать имя:

type StuffBody = {
  [string]: string,
};
0

В вашем вопросе вы определяете интерфейс Settings, который включает в себя два ключа: lang (может принимать значения 'en' или 'da') и welcome (логическое значение). Затем вы хотите создать метод setSettings, который будет принимать ключ в качестве параметра и будет ограничен ключами интерфейса Settings.

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

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

private setSettings(key: keyof Settings, value: Settings[keyof Settings]) {
   // Здесь вы можете обновить настройки по ключу
   this.settings[key] = value;
}

В данном примере keyof Settings обеспечивает, что параметр key может быть только одним из ключей интерфейса Settings. Также, чтобы гарантировать правильный тип значения value, мы используем Settings[keyof Settings], что ограничивает тип значения, соответствующим типу ключа в интерфейсе Settings.

Также убедитесь, что у вас есть свойство settings, где будут храниться ваши настройки.

0

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

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

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

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

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

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

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

let staff: Map<string, string> = new Map();
Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь