- Индексные сигнатуры¶
- Индексные сигнатуры в TypeScript¶
- Объявление индексной сигнатуры¶
- Все члены должны соответствовать сигнатуре индекса string ¶
- Использование ограниченного набора строковых литералов¶
- Используйте и строки и числа в качестве индексов¶
- Шаблон проектирования: вложенная индексная сигнатура¶
- Исключение определенных свойств из сигнатуры индекса¶
Индексные сигнатуры¶
К Object в JavaScript (и, следовательно, в TypeScript) можно получить доступ с помощью строки, содержащей ссылку на любой другой JavaScript объект.
let foo: any = <>; foo['Hello'] = 'World'; console.log(foo['Hello']); // World
Мы храним строку «World» под ключом «Hello» . Помните, мы говорили, что он может хранить любой JavaScript объект, поэтому давайте сохраним экземпляр класса, чтобы показать концепцию:
class Foo constructor(public message: string) <> log() console.log(this.message); > > let foo: any = <>; foo['Hello'] = new Foo('World'); foo['Hello'].log(); // World
Помните мы сказали, что к нему можно получить доступ с помощью строки. Если вы передадите в сигнатуру индекса любой другой объект, среда выполнения JavaScript вызовет для него .toString перед получением результата. Это показано ниже:
let obj = toString() console.log('toString вызывается'); return 'Hello'; >, >; let foo: any = <>; foo[obj] = 'World'; // toString вызывается console.log(foo[obj]); // toString вызывается, World console.log(foo['Hello']); // World
Обратите внимание, что toString будет вызываться всякий раз, когда obj используется в позиции индекса.
Массивы немного отличаются. Для индексации числа виртуальные машины JavaScript будут пытаться оптимизировать (в зависимости от того, действительно ли это массив, совпадают ли структуры хранимых элементов и т.д.). Таким образом, число следует рассматривать как действительный метод доступа к объекту сам по себе (в отличие от строки ). Вот простой пример c массивом:
let foo = ['World']; console.log(foo[0]); // World
Итак, это JavaScript. Теперь давайте посмотрим на изящную обработку этой концепции в TypeScript.
Индексные сигнатуры в TypeScript¶
Во-первых, поскольку JavaScript неявно вызывает toString для любой сигнатуры индекса в виде объекта, TypeScript выдаст вам ошибку, чтобы новички не стреляли себе в ногу (я вижу, как пользователи стреляют себе в ногу при постоянном использовании JavaScript на stackoverflow):
1 2 3 4 5 6 7 8 9 10 11 12 13
let obj = toString() return 'Hello'; >, >; let foo: any = <>; // ОШИБКА: сигнатура индекса должна быть строкой, числом . foo[obj] = 'World'; // ИСПРАВЛЕНИЕ: TypeScript заставляет вас быть явным foo[obj.toString()] = 'World';
Причина для такого принуждения пользователя быть явным в том, что реализация toString по умолчанию для объекта довольно ужасна, например в v8 он всегда возвращает [object Object] :
let obj = message: 'Hello' >; let foo: any = <>; // ОШИБКА: сигнатура индекса должна быть строкой, числом . foo[obj] = 'World'; // Вот где вы на самом деле хранили! console.log(foo['[object Object]']); // World
Конечно, поддерживается число , потому что
- Это необходимо для отличной поддержки массивов / кортежей.
- Даже если вы используете его для obj , его реализация toString по умолчанию нормальна (не [object Object] ).
console.log((1).toString()); // 1 console.log((2).toString()); // 2
Сигнатуры индекса TypeScript должны быть либо строкой, либо числом.
Краткое примечание: символы также действительны и поддерживаются TypeScript. Но пока не будем туда заходить. Будем двигаться маленькими шажками.
Объявление индексной сигнатуры¶
Итак, мы использовали any , чтобы сказать TypeScript, что мы можем делать все, что захотим. Фактически мы можем явно указать сигнатуру для index. Например, скажем, вы хотите убедиться, что все, что хранится в объекте с использованием строки, соответствует структуре . Это можно сделать с помощью объявления < [index:string] : > . Это показано ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
let foo: [index: string]: message: string > > = <>; /** * Необходимо хранить в соответствии структуре */ /** Ok */ foo['a'] = message: 'some message' >; /** Ошибка: должно содержать `message` типа string. У вас опечатка в * `message` */ foo['a'] = messages: 'some message' >; /** * Тип также проверяется на чтение */ /** Ok */ foo['a'].message; /** Ошибка: messages не существует. У вас опечатка в `message` */ foo['a'].messages;
СОВЕТ: имя сигнатуры индекса, например index в < [index:string] : > не имеет значения для TypeScript и предназначен только для удобства чтения. Например если это имена пользователей, вы можете использовать < [username:string] : > , чтобы помочь следующему разработчику, который просматривает код (который кстати может оказаться вами).
Конечно, числовые индексы также поддерживаются, например
Все члены должны соответствовать сигнатуре индекса string ¶
Как только у вас есть сигнатура индекса string , все явные элементы также должны соответствовать этой сигнатуре индекса. Это показано ниже:
1 2 3 4 5 6 7 8 9 10 11 12
/** Okay */ interface Foo [key: string]: number; x: number; y: number; > /** Ошибка */ interface Bar [key: string]: number; x: number; y: string; // ОШИБКА: Свойство `y` должно иметь тип number >
Это сделано для обеспечения безопасности, чтобы любой доступ к строке давал одинаковый результат:
1 2 3 4 5 6 7 8 9 10 11 12
interface Foo [key: string]: number; x: number; > let foo: Foo = x: 1, y: 2 >; // Напрямую foo['x']; // число // Опосредственно let x = 'x'; foo[x]; // число
Использование ограниченного набора строковых литералов¶
Сигнатура индекса может требовать, чтобы строки индекса были частями объединения литеральных строк, используя замапленные типы, например:
type Index = 'a' | 'b' | 'c'; type FromIndex = [k in Index]?: number >; const good: FromIndex = b: 1, c: 2 >; // Ошибка: // Тип '< b: number; c: number; d: number; >' нельзя присвоить типу 'FromIndex'. // Литерал объекта может указывать только известные свойства, а 'd' // не существует в типе 'FromIndex'. const bad: FromIndex = b: 1, c: 2, d: 3 >;
Это часто используется вместе с keyof typeof для захвата типов словаря, описанных далее.
В общем случае определение словаря может быть отложено:
type FromSomeIndexK extends string> = [key in K]: number; >;
Используйте и строки и числа в качестве индексов¶
Это не стандартный вариант использования, но компилятор TypeScript, тем не менее, его поддерживает.
Однако у него есть ограничение: индексатор string более строгий, чем индексатор number . Это сделано намеренно, например чтобы разрешить следующий код:
interface ArrStr [key: string]: string | number; // Должен согласовываться со всеми // элементами [index: number]: string; // Может быть подмножеством индексатора строк // Просто пример элемента length: number; >
Шаблон проектирования: вложенная индексная сигнатура¶
Рекомендации для API при добавлении индексных сигнатур
Довольно часто в сообществе JS можно встретить API, которые неправильно употребляют строковые индексаторы. Например стандартный шаблон для CSS в библиотеках JS:
interface NestedCSS color?: string; [selector: string]: string | NestedCSS | undefined; > const example: NestedCSS = color: 'red', '.subclass': color: 'blue', >, >;
Постарайтесь не смешивать таким образом индексаторы строк с валидными значениями. Например, опечатка останется невыявленной:
const failsSilently: NestedCSS = colour: 'red', // Ошибок нет, так как colour является допустимым // селектором строки >;
Вместо этого поместите вложение в отдельное свойство, например в имя nest (или children , или subnodes и т.д.):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
interface NestedCSS color?: string; nest?: [selector: string]: NestedCSS; >; > const example: NestedCSS = color: 'red', nest: '.subclass': color: 'blue', >, >, >; const failsSilently: NestedCSS = colour: 'red', // Ошибка TS: неизвестное свойство `colour` >;
Исключение определенных свойств из сигнатуры индекса¶
Иногда вам нужно объединить свойства в сигнатуру индекса. Это не рекомендуется, и вам следует использовать упомянутый выше шаблон вложенной индексной сигнатуры.
Однако, если вы моделируете существующий JavaScript, вы можете обойти это с помощью типа пересечения. Ниже показан пример ошибки, с которой вы столкнетесь без использования пересечения:
type FieldState = value: string; >; type FormState = isValid: boolean; // Ошибка: не соответствует сигнатуре индекса [fieldName: string]: FieldState; >;
Вот обходной путь с использованием типа пересечения:
type FieldState = value: string; >; type FormState = isValid: boolean > & [fieldName: string]: FieldState; >;
Обратите внимание, что даже если вы можете объявить его для моделирования существующего JavaScript, вы не можете создать такой объект с помощью TypeScript:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
type FieldState = value: string; >; type FormState = isValid: boolean > & [fieldName: string]: FieldState; >; // Используйте это для какого-нибудь объекта JavaScript, который вы откуда-то // получаете declare const foo: FormState; const isValidBool = foo.isValid; const somethingFieldState = foo['something']; // Использование его для создания объекта TypeScript не сработает const bar: FormState = // Ошибка: `isValid` не может быть присвоен `FieldState` isValid: false, >;