Посилення типу індексованих членів об’єкта Typescript?


290

Я хотів би зберегти відображення рядка -> рядка в об'єкті Typescript і застосувати, щоб усі клавіші відображалися в рядках. Наприклад:

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

Чи є спосіб я встановити, що значення повинні бути рядками (або будь-яким іншим типом ..)?

Відповіді:


521
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'тип індексування (ключ)?
— calebboyd

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; } не є typesafe, оскільки внутрішні ключі об'єкта завжди є рядком - детальніше див. коментар до питання від @tep. наприклад, Запуск x = {}; x[1] = 2;у Chrome потім Object.keys(x)повертає ["1"] і JSON.stringify(x)повертає "{" 1 ": 2} '. Кутові корпуси з typeof Number(наприклад, Infinity, NaN, 1e300, 99999999999999999999999 тощо) перетворюються на рядкові ключі. Також остерігайтеся інших випадків кутових для строкових ключів , як x[''] = 'empty string';, x['000'] = 'threezeros'; x[undefined] = 'foo'.
— robocat

@robocat Це правда, і я переходив до редагування, щоб на деякий час видалити інтерфейси з цифровими ключами з цієї відповіді. Зрештою, я вирішив зберегти їх, оскільки TypeScript технічно і спеціально дозволяє цифри як ключі. Сказавши це, я сподіваюся, що кожен, хто вирішить використовувати об’єкти, індексовані цифрами, побачить ваш коментар.
— Сенді Гіффорд

75

Швидке оновлення: оскільки 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 підтримується більшістю платформ, майже завжди краще дотримуватися карти, коли вам потрібно пов’язати деякі дані з якимсь ключем.

Коли ми пишемо, let a: { [s: string]: string; }нам потрібно пам’ятати, що після компіляції машинопису не існує такого поняття, як дані типу, він використовується лише для компіляції. І {[s: string]: string; } складеться до {}.

Це сказало, навіть якщо ви напишете щось на кшталт:

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").
— Роман Старков

Особисто мені подобається рідний формат машинопису, але ти маєш право найкращим чином використовувати стандарт. Це дає мені змогу задокументувати ключ "ім'я", який насправді не є корисним, але я можу зробити ключ, який називається чимось на кшталт "patiId" :)
— кодування

Ця відповідь абсолютно вірна і дає дуже хороші моменти, але я не погоджуюся з думкою, що майже завжди краще дотримуватися рідних 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.