Оглавление:
Карта сайта:
Оглавление:
Карта сайта:
Впервые реализуя динамические ключи в статически типизированном TypeScript, могут возникнуть трудности, которые вместе с сопряженными тонкостями, будут подробно рассмотрены в этой главе.
Статический анализ кода всеми силами стремится взять под контроль синтаксические конструкции, тем самым переложить работу, связанную с выявлением ошибок, на себя, оставляя разработчику больше времени на более важные задачи. И несмотря на то, что динамические операции являются причиной “головной боли” компилятора, потребность в них при разработке программ все-таки существует. Одной из таких операций является определение в объектах индексных членов (динамических ключей).
Индексная сигнатура (index signature) состоит из двух частей. В первой части расположен имеющий собственную аннотацию типа идентификатор привязки (binding identifier), заключенный в квадратные скобки []. Во второй части расположена аннотация типа (type annotation), представляющего значение, ассоциируемое с динамическим ключом.
{ [identifier: Type]: Type }
При объявлении индексной сигнатуры не должны быть использованы модификаторы доступа и модификатор static, а идентификатор привязки должен принадлежать к типу string или number. В качестве типа, указанного справа от двоеточия, может быть указан любой тип, а идентификатор привязки иметь произвольное имя.
interface Identifier { [identifier: string]: string; } // или interface Identifier { [key: number]: string; } // или interface Identifier { [name: number]: string; }
В одном объектном типе одновременно могут быть объявлены индексные сигнатуры, чьи идентификаторы привязки принадлежат к типу string и типу number. Но с одной оговоркой. Их типы, указанные в аннотации типов, должны быть совместимы (совместимость типов подробно рассматривается в главах “Типизация - Совместимость объектов” и “Типизация - Совместимость функций”).
interface A { [key: string]: string; [key: number]: string; } let a: A = { validKeyDeclareStatic: 'value', // Ok, значение принадлежит к string invalidKeyDeclareStatic: 0, // Error, значение должно быть совместимым с типом string }; a.validKeyDefineDynamicKey = 'value'; // Ok a.invalidKeyDefineDynamicKey = 0; // Error, значение должно быть совместимым с типом string a[0] = 'value'; // Ok interface B { [identifier: string]: string; // Ok [identifier: string]: string; // Error, дубликат } interface С { [identifier: string]: string; // Ok [identifier: number]: number; // Error, должен принадлежать к типу string } class SuperClass { // суперкласс a: number; } class SubClass extends SuperClass { // подкласс b: number; } interface D { [identifier: string]: SuperClass; // Ok [identifier: number]: SubClass; // Ok, SubClass совместим с SuperClass } let d: D = {}; d.dynamicKey = new SubClass(); // Ok d[0] = new SubClass(); // Ok interface E { [identifier: string]: SubClass; // Ok [identifier: number]: SuperClass; // Error, SuperClass несовместим с SubClass }
Так как классы принадлежат к объектным типам, их тела также могут определять индексные сигнатуры. Правила, относящиеся к индексным сигнатурам, которые были и будут рассмотрены в этой главе, в полной мере справедливы и для классов.
class Identifier { [key: string]: string; [key: number]: string; [0]: 'value'; [1]: 5; // Error, все члены должны принадлежать к совместимым со string типам public a: string = 'value'; // Ok, поле name с типом string public b: number = 0; // Error, все члены должны принадлежать к совместимым со string типам public c(): void {} // Error, метод тоже член и на него распространяются те же правила } let identifier: Identifier = new Identifier(); identifier.validDynamicKey = 'value'; // Ok identifier.invalidDynamicKey = 0; // Error identifier[2] = 'value'; // Ok identifier[3] = 0; // Error
Кроме того, классы накладывают ограничение, не позволяющее использовать модификаторы доступа (private, protected, public), а также модификаторы, указывающие на принадлежность к уровню класса (static). При попытке указать данные модификаторы для индексной сигнатуры возникнет ошибка.
class A { public [key: string]: string; // Error } class B { static [key: string]: string; // Error }
Но, относительно модификаторов, есть несколько нюансов, связанных с модификатором readonly, который подробно рассматривается в главе “Классы - Модификатор readonly”. Чтобы ничего не ускользнуло от понимания, начнем по порядку.
При указании модификатора readonly искажается смысл использования индексной сигнатуры, так как это дает ровно противоположный эффект. Вместо объекта с возможностью определения динамических членов получается объект, позволяющий лишь объявлять динамические ключи, которым нельзя ничего присвоить. То есть, объект полностью закрыт для изменения.
В случае с интерфейсом:
interface IIdentifier { readonly [key: string]: string; // Ok, модификатор readonly } let instanceObject: IIdentifier = {}; instanceObject.a; // Ok, можно объявить instanceObject.a = 'value'; // Error, но нельзя присвоить значение
В случае с классом:
class Identifier { readonly [key: string]: string; } let instanseClass = new Identifier(); instanseClass.a; // Ok, можно объявить instanseClass.a = 'value'; // Error, но нельзя присвоить значение
Второй нюанс заключается в том, что, если в объекте или классе определена индексная сигнатура, становится невозможным объявить в их теле или добавить через точечную и скобочную нотацию ключи, ассоциированные с несовместимым типом данных. Простыми словами - тело объекта или класса, имеющего определение индексной сигнатуры, не может иметь определения членов других типов.
В случае с интерфейсом:
interface IIdentifier { [key: string]: string; a: string; // Ok, [в момент декларации] b: number; // Error, [в момент декларации] допускается объявление идентификаторов, принадлежащих только к типу string } let instanceObject: IIdentifier = { c: '', // Ok, [в момент объявления] d: 0, // Error, [в момент объявления] допускается объявление идентификаторов, принадлежащих только к типу string }; instanceObject.e = ''; // Ok, [после объявления] instanceObject.f = 0; // Error, [после объявления] допускается объявление идентификаторов, принадлежащих только к типу string
В случае с классом:
class Identifier { [key: string]: string; a: string; // Ok, [в момент объявления] b: number; // Error, [в момент объявления] допускается объявление идентификаторов, принадлежащих только к типу string } let instanseClass = new Identifier(); instanseClass.c = ''; // Ok, [после объявления] instanseClass.d = 0; // Error, [после объявления] допускается объявление идентификаторов, принадлежащих только к типу string
Но, в случае с модификатором readonly, поведение отличается. Несмотря на то, что указывать идентификаторы членов, принадлежащие к несовместимым типам, по-прежнему нельзя, допускается их декларация и объявление.
В случае с интерфейсом:
interface IIdentifier { readonly [key: string]: string; // Ok a: string; // Ok, декларация } let instanceObject: IIdentifier = { a: '', // Ok, объявление b: '', // Ok, объявление }; instanceObject.с = 'value'; // Error, ассоциировать ключ со значением после создания объекта по-прежнему нельзя
В случае с классом:
class Identifier { readonly [key: string]: string; a: string = 'value'; // Ok, декларация и объявление } let instanseClass = new Identifier(); instanseClass.b = 'value'; // Error, ассоциировать ключ со значением после создания объекта по-прежнему нельзя
К тому же, объекты и классы, имеющие определение индексной сигнатуры, не могут содержать определения методов.
interface IIdentifier { readonly [key: string]: string; method(): void; // Error -> TS2411: Property 'method' of type '() => void' is not assignable to string index type 'string' } class Identifier { readonly [key: string]: string; method(): void {} // Error -> TS2411: Property 'method' of type '() => void' is not assignable to string index type 'string'. }
Третий нюанс проистекает от предыдущего и заключается в том, что значения, ассоциированные с идентификаторами, которые были задекларированы в типе, можно перезаписать после инициализации объекта.
В случае с интерфейсом:
interface IIdentifier { readonly [key: string]: string; // Ok a: string; // Ok, декларация } let instanceObject: IIdentifier = { a: 'value', // Ok, реализация b: 'value', // Ok, объявление }; instanceObject.a = 'new value'; // Ok, можно перезаписать значение instanceObject.b = 'new value'; // Error, нельзя перезаписать значение
В случае с классом:
class Identifier { readonly [key: string]: string; a: string = 'value'; // Ok, декларация и объявление } let instanseClass = new Identifier(); instanseClass.a = 'new value'; // Ok, можно перезаписать значение
Если учесть, что модификатор readonly вообще не стоит применять к индексной сигнатуре, то все эти нюансы вообще можно выкинуть из головы. Но, так как цель этой книги защитить читателя от как можно большего количества подводных камней, я решил не опускать данный факт, ведь знание — лучшая защита.
Кроме того, не будет лишним знать наперед, что если идентификатор привязки принадлежит к типу string, то в качестве ключа может быть использовано значение, принадлежащее к типам string, number, symbol, Number Enum и String Enum.
interface StringDynamicKey { [key: string]: string; } enum NumberEnum { Prop = 0, } enum StringEnum { Prop = 'prop', } let example: StringDynamicKey = { property: '', // Ok String key '': '', // Ok String key 1: '', // Ok Number key [Symbol.for('key')]: '', // Ok Symbol key [NumberEnum.Prop]: '', // Ok Number Enum key [StringEnum.Prop]: '', // Ok String Enum key };
В случае, когда идентификатор привязки принадлежит к типу number, то значение, используемое в качестве ключа, может принадлежать к таким типам, как number, symbol, Number Enum и String Enum.
interface NumberDynamicKey { [key: number]: string; } enum NumberEnum { Prop = 0, } enum StringEnum { Prop = 'prop', } let example: NumberDynamicKey = { property: '', // Error String key '': '', // Error String key 1: '', // Ok Number key [Symbol.for('key')]: '', // Ok Symbol key [NumberEnum.Prop]: '', // Ok Number Enum key [StringEnum.Prop]: '', // Ok String Enum key };
Вывод типов, в некоторых случаях, выводит тип, принадлежащий к объектному типу с индексной сигнатурой. Напомню, что в JavaScript, помимо привычного способа при объявлении идентификаторов в объектных типах, можно использовать строковые литералы и выражения, заключённые в квадратные скобки [].
let computedIdentifier = 'e'; let v = { a: '', // объявление идентификатора привычным способом, ['b']: '', // объявление идентификатора с помощью строкового литерала. ['c' + 'd']: '', // объявление идентификатора с помощью выражения со строковыми литералами [computedIdentifier]: '', // объявление идентификатора при помощи вычисляемого идентификатора };
В первых двух случаях, вывод типов выводит ожидаемый тип, а в оставшихся тип с индексной сигнатурой.
// let v1: { a: string; } let v1 = { a: 'value', // Ok, привычный идентификатор }; v1.b = 'value'; // Error, в типе { a: string } не задекларирован идентификатор b
// let v2: { ['a']: string; } let v2 = { ['a']: 'value', // Ok, строковый литерал }; v2.b = 'value'; // Error, в типе { ['a']: string } не задекларирован идентификатор b
let computedIdentifier: string = 'a'; // let v3: { [x: string]: string }; - вывод типов выводит как тип с индексной сигнатурой... let v3 = { [computedIdentifier]: 'value', // вычисляемое свойство }; v3.b = 'value'; // ... а это, в свою очередь, позволяет добавлять новое значение
// let v4: { [x: string]: string }; - вывод типов выводит как тип с индексной сигнатурой... let v4 = { ['a' + 'b']: 'value', // выражение со строковыми литералами }; v4.b = 'value'; // ... а это, в свою очередь, позволяет добавлять новое значение
Поскольку механизм, позволяющий определение индексной сигнатуры, не способен отслеживать идентификаторы (имена) полей, определенных динамически, такой подход не считается типобезопасным.
type T = { [key: string]: number | string; }; function f(p: T) { /** * Обращение к несуществующим полям */ p.bad.toString(); // Ok -> Ошибка времени исполнения p[Math.random()].toString(); // Ok -> Ошибка времени исполнения }
Данная проблема решается с помощью флага –noUncheckedIndexedAccess, активирующего строгую проверку при обращении к динамическим членам объектных типов. Флаг –noUncheckedIndexedAccess ожидает в качестве значения true либо false. Активация механизма позволяет обращаться к динамическим членам только после подтверждения их наличия в объекте, a также совместно с такими операторами, как оператор опциональной последовательности ?. и опциональный оператор !..
// @filename: tsconfig.json { "compilerOptions": { "noUncheckedIndexedAccess": true } }
type T = { [key: string]: number | string; }; function f(p: T) { /** * Обращение к несуществующим полям */ p.bad.toString(); // Error -> TS2532: Object is possibly 'undefined'. p[Math.random()].toString(); // Error -> TS2532: Object is possibly 'undefined'. // Проверка наличия поля bad if ('bad' in p) { p.bad?.toString(); // Ok } // Использование опционального оператора p[Math.random()]!.toString(); // Ok -> ошибка во время выполнения p[Math.random()]?.toString(); // Ok -> Ошибка не возникнет }
Не будет лишним упомянуть, что влияние данного механизма распространяется также и на массивы. В случае с массивом не получится избежать аналогичной ошибки при попытке обращения к его элементам при помощи индексной сигнатуры.
function f(array: string[]) { for (let i = 0; i < array.length; i++) { array[i].toString(); // Error -> TS2532: Object is possibly 'undefined'. } }
Поскольку к динамическим ключам можно обращаться как через точечную, так и скобочную нотацию, то может возникнуть потребность в разделении такого поведения. Для подобных случаев в TypeScript существует флаг –noPropertyAccessFromIndexSignature, установление которого в значение true запрещает обращение к динамическим членам с помощью точечной нотации.
// @filename: tsconfig.json { "compilerOptions": { "noPropertyAccessFromIndexSignature": "true" } }
type Settings = { env?: string[]; // определение необязательного предопределенного поля [key: string]: any; // определение динамических полей }; function configurate(settings: Settings) { //--------------------------- // динамическое поле if (settings.envs) { // Ошибка при активном флаге и Ok при неактивном } if (settings['envs']) { // Ok при любом значении флага } //---------------------------- // предопределенное поле if (settings.env) { // Ok [1] } if (settings['env']) { // Ok при любом значении флага } }
Объектные типы, определяющие строковую индексную сигнатуру, считаются совместимыми с объектными типами, имеющими необязательные поля.
/** * [0] поля определены как необязательные! */ type Magic = { fire?: string[]; water?: string[]; }; declare const HERO_CONFIG: Magic; const hero: { [key: string]: string[] } = HERO_CONFIG; // Ok /**Ok*/
Но существует два неочевидных момента. При попытке инициализировать необязательные поля значением undefined возникнет ошибка.
/** * [0] поля определены как необязательные! */ type Magic = { fire?: string[]; water?: string[]; }; declare const HERO_CONFIG: Magic; /** * [1] Error -> * Type 'undefined' is not assignable to type 'string[]'. */ const hero: { [key: string]: string[] } = HERO_CONFIG; hero['fire'] = ['fireball']; // Ok hero['water'] = undefined; // Error [1]
Кроме этого, ошибки не удастся избежать тогда, когда объектный тип, вместо опционального оператора, явно указывает принадлежность типа членов к типу undefined.
/** * [0] поля определены как обязательные! */ type Magic = { fire: string[] | undefined; // [0] water: string[] | undefined; // [0] }; declare const HERO_CONFIG: Magic; /** * [1] Error -> * Type 'Magic' is not assignable to type '{ [key: string]: string[]; }'. */ const hero: { [key: string]: string[] } = HERO_CONFIG; /**[1]*/
И кроме того, данные правила применимы исключительно к строковой индексной сигнатуре.
/** * [0] ключи полей определены как индексы! */ type Port = { 80?: string; // [0] 88?: string; // [0] }; declare const SERVER_PORT: Port; /** * [2] Ключ индексной сигнатуры принадлежит к типу number. * * [1] Error -> * Type 'Port' is not assignable to type '{ [key: number]: string[]; }'. */ const port: { [key: number]: string[]; } = SERVER_PORT; /**[2] */ /**[1] */