"Как задать тип для индексируемых членов объекта в TypeScript?"
Я хотел бы сохранить отображение строк -> строк в объекте TypeScript и обеспечить, чтобы все значения соответствовали строкам. Например:
var stuff = {};
stuff["a"] = "foo"; // все в порядке
stuff["b"] = "bar"; // все в порядке
stuff["c"] = false; // ОШИБКА! bool != string
Существует ли способ заставить TypeScript проверять, чтобы значения были строками (или любым другим типом)?
5 ответ(ов)
Ваш вопрос касается использования объекта в TypeScript с индексной сигнатурой. Вот перевод вашего кода с объяснением:
var stuff: { [key: string]: string; } = {};
stuff['a'] = ''; // все в порядке
stuff['a'] = 4; // ошибка
// ... или, если вы используете это часто и не хотите много писать ...
interface StringMap { [key: string]: string; }
var stuff2: StringMap = { };
// то же самое, как и выше
В данном примере вы создаете объект stuff
, который имеет индексную сигнатуру, указывающую, что ключи должны быть строками, а соответствующие значения также должны быть строками. При попытке присвоить числовое значение 4
возникнет ошибка, поскольку оно не соответствует типу string
.
Если вы используете подобные структуры данных достаточно часто, вы можете определить интерфейс, как в примере с StringMap
. Это сделает код более читаемым и уменьшит количество вводимых символов, не теряя при этом строгости типов.
Интерфейс 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 делает их совершенно неактуальными. Если вы всё же хотите их использовать, посмотрите историю правок.
Вы можете передать имя неизвестному ключу и затем указать свои типы:
type StuffBody = {
[key: string]: string;
};
Теперь вы можете использовать это в проверке типов:
let stuff: StuffBody = {};
Однако для FlowType нет необходимости указывать имя:
type StuffBody = {
[string]: string,
};
В вашем вопросе вы определяете интерфейс 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
, где будут храниться ваши настройки.
Ответ @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();
Как конвертировать строку в enum в TypeScript?
'unknown' против 'any': в чем разница?
Как получить имена элементов перечисления (enum)?
@Directive против @Component в Angular
Не удается использовать JSX, если не указан флаг '--jsx'