Впервые реализуя динамические ключи в статически типизированном TypeScript, могут возникнуть трудности, которые вместе с сопряженными тонкостями, будут подробно рассмотрены в этой главе.
Индексные члены (определение динамических ключей)¶
Статический анализ кода всеми силами стремится взять под контроль синтаксические конструкции, тем самым переложить работу, связанную с выявлением ошибок, на себя, оставляя разработчику больше времени на более важные задачи. И несмотря на то, что динамические операции являются причиной “головной боли” компилятора, потребность в них при разработке программ все-таки существует. Одной из таких операций является определение в объектах индексных членов (динамических ключей).
Индексная сигнатура (index signature) состоит из двух частей. В первой части расположен имеющий собственную аннотацию типа идентификатор привязки (binding identifier), заключенный в квадратные скобки []. Во второй части расположена аннотация типа (type annotation), представляющего значение, ассоциируемое с динамическим ключом.
1
{[identifier:Type]:Type}
При объявлении индексной сигнатуры не должны быть использованы модификаторы доступа и модификатор static, а идентификатор привязки должен принадлежать к типу string или number. В качестве типа, указанного справа от двоеточия, может быть указан любой тип, а идентификатор привязки иметь произвольное имя.
В одном объектном типе одновременно могут быть объявлены индексные сигнатуры, чьи идентификаторы привязки принадлежат к типу string и типу number. Но с одной оговоркой. Их типы, указанные в аннотации типов, должны быть совместимы (совместимость типов подробно рассматривается в главах “Типизация - Совместимость объектов” и “Типизация - Совместимость функций”).
interfaceA{[key:string]:string;[key:number]:string;}leta:A={validKeyDeclareStatic:'value',// Ok, значение принадлежит к stringinvalidKeyDeclareStatic:0,// Error, значение должно быть совместимым с типом string};a.validKeyDefineDynamicKey='value';// Oka.invalidKeyDefineDynamicKey=0;// Error, значение должно быть совместимым с типом stringa[0]='value';// OkinterfaceB{[identifier:string]:string;// Ok[identifier:string]:string;// Error, дубликат}interfaceС{[identifier:string]:string;// Ok[identifier:number]:number;// Error, должен принадлежать к типу string}classSuperClass{// суперклассa:number;}classSubClassextendsSuperClass{// подклассb:number;}interfaceD{[identifier:string]:SuperClass;// Ok[identifier:number]:SubClass;// Ok, SubClass совместим с SuperClass}letd:D={};d.dynamicKey=newSubClass();// Okd[0]=newSubClass();// OkinterfaceE{[identifier:string]:SubClass;// Ok[identifier:number]:SuperClass;// Error, SuperClass несовместим с SubClass}
Так как классы принадлежат к объектным типам, их тела также могут определять индексные сигнатуры. Правила, относящиеся к индексным сигнатурам, которые были и будут рассмотрены в этой главе, в полной мере справедливы и для классов.
1 2 3 4 5 6 7 8 910111213141516171819
classIdentifier{[key:string]:string;[key:number]:string;[0]:'value';[1]:5;// Error, все члены должны принадлежать к совместимым со string типамpublica:string='value';// Ok, поле name с типом stringpublicb:number=0;// Error, все члены должны принадлежать к совместимым со string типамpublicc():void{}// Error, метод тоже член и на него распространяются те же правила}letidentifier:Identifier=newIdentifier();identifier.validDynamicKey='value';// Okidentifier.invalidDynamicKey=0;// Erroridentifier[2]='value';// Okidentifier[3]=0;// Error
Кроме того, классы накладывают ограничение, не позволяющее использовать модификаторы доступа (private, protected, public), а также модификаторы, указывающие на принадлежность к уровню класса (static). При попытке указать данные модификаторы для индексной сигнатуры возникнет ошибка.
Но, относительно модификаторов, есть несколько нюансов, связанных с модификатором readonly, который подробно рассматривается в главе “Классы - Модификатор readonly”. Чтобы ничего не ускользнуло от понимания, начнем по порядку.
При указании модификатора readonly искажается смысл использования индексной сигнатуры, так как это дает ровно противоположный эффект. Вместо объекта с возможностью определения динамических членов получается объект, позволяющий лишь объявлять динамические ключи, которым нельзя ничего присвоить. То есть, объект полностью закрыт для изменения.
В случае с интерфейсом:
12345678
interfaceIIdentifier{readonly[key:string]:string;// Ok, модификатор readonly}letinstanceObject:IIdentifier={};instanceObject.a;// Ok, можно объявитьinstanceObject.a='value';// Error, но нельзя присвоить значение
В случае с классом:
1234567
classIdentifier{readonly[key:string]:string;}letinstanseClass=newIdentifier();instanseClass.a;// Ok, можно объявитьinstanseClass.a='value';// Error, но нельзя присвоить значение
Второй нюанс заключается в том, что, если в объекте или классе определена индексная сигнатура, становится невозможным объявить в их теле или добавить через точечную и скобочную нотацию ключи, ассоциированные с несовместимым типом данных. Простыми словами - тело объекта или класса, имеющего определение индексной сигнатуры, не может иметь определения членов других типов.
В случае с интерфейсом:
1 2 3 4 5 6 7 8 91011121314
interfaceIIdentifier{[key:string]:string;a:string;// Ok, [в момент декларации]b:number;// Error, [в момент декларации] допускается объявление идентификаторов, принадлежащих только к типу string}letinstanceObject:IIdentifier={c:'',// Ok, [в момент объявления]d:0,// Error, [в момент объявления] допускается объявление идентификаторов, принадлежащих только к типу string};instanceObject.e='';// Ok, [после объявления]instanceObject.f=0;// Error, [после объявления] допускается объявление идентификаторов, принадлежащих только к типу string
В случае с классом:
1 2 3 4 5 6 7 8 910
classIdentifier{[key:string]:string;a:string;// Ok, [в момент объявления]b:number;// Error, [в момент объявления] допускается объявление идентификаторов, принадлежащих только к типу string}letinstanseClass=newIdentifier();instanseClass.c='';// Ok, [после объявления]instanseClass.d=0;// Error, [после объявления] допускается объявление идентификаторов, принадлежащих только к типу string
Но, в случае с модификатором readonly, поведение отличается. Несмотря на то, что указывать идентификаторы членов, принадлежащие к несовместимым типам, по-прежнему нельзя, допускается их декларация и объявление.
В случае с интерфейсом:
1 2 3 4 5 6 7 8 9101112
interfaceIIdentifier{readonly[key:string]:string;// Oka:string;// Ok, декларация}letinstanceObject:IIdentifier={a:'',// Ok, объявлениеb:'',// Ok, объявление};instanceObject.с='value';// Error, ассоциировать ключ со значением после создания объекта по-прежнему нельзя
В случае с классом:
12345678
classIdentifier{readonly[key:string]:string;a:string='value';// Ok, декларация и объявление}letinstanseClass=newIdentifier();instanseClass.b='value';// Error, ассоциировать ключ со значением после создания объекта по-прежнему нельзя
К тому же, объекты и классы, имеющие определение индексной сигнатуры, не могут содержать определения методов.
1 2 3 4 5 6 7 8 91011
interfaceIIdentifier{readonly[key:string]:string;method():void;// Error -> TS2411: Property 'method' of type '() => void' is not assignable to string index type 'string'}classIdentifier{readonly[key:string]:string;method():void{}// Error -> TS2411: Property 'method' of type '() => void' is not assignable to string index type 'string'.}
Третий нюанс проистекает от предыдущего и заключается в том, что значения, ассоциированные с идентификаторами, которые были задекларированы в типе, можно перезаписать после инициализации объекта.
В случае с интерфейсом:
1 2 3 4 5 6 7 8 910111213
interfaceIIdentifier{readonly[key:string]:string;// Oka:string;// Ok, декларация}letinstanceObject:IIdentifier={a:'value',// Ok, реализацияb:'value',// Ok, объявление};instanceObject.a='new value';// Ok, можно перезаписать значениеinstanceObject.b='new value';// Error, нельзя перезаписать значение
В случае с классом:
12345678
classIdentifier{readonly[key:string]:string;a:string='value';// Ok, декларация и объявление}letinstanseClass=newIdentifier();instanseClass.a='new value';// Ok, можно перезаписать значение
Если учесть, что модификатор readonly вообще не стоит применять к индексной сигнатуре, то все эти нюансы вообще можно выкинуть из головы. Но, так как цель этой книги защитить читателя от как можно большего количества подводных камней, я решил не опускать данный факт, ведь знание — лучшая защита.
Кроме того, не будет лишним знать наперед, что если идентификатор привязки принадлежит к типу string, то в качестве ключа может быть использовано значение, принадлежащее к типам string, number, symbol, Number Enum и String Enum.
1 2 3 4 5 6 7 8 91011121314151617181920
interfaceStringDynamicKey{[key:string]:string;}enumNumberEnum{Prop=0,}enumStringEnum{Prop='prop',}letexample:StringDynamicKey={property:'',// Ok String key'':'',// Ok String key1:'',// 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.
1 2 3 4 5 6 7 8 91011121314151617181920
interfaceNumberDynamicKey{[key:number]:string;}enumNumberEnum{Prop=0,}enumStringEnum{Prop='prop',}letexample:NumberDynamicKey={property:'',// Error String key'':'',// Error String key1:'',// Ok Number key[Symbol.for('key')]:'',// Ok Symbol key[NumberEnum.Prop]:'',// Ok Number Enum key[StringEnum.Prop]:'',// Ok String Enum key};
Вывод типов, в некоторых случаях, выводит тип, принадлежащий к объектному типу с индексной сигнатурой. Напомню, что в JavaScript, помимо привычного способа при объявлении идентификаторов в объектных типах, можно использовать строковые литералы и выражения, заключённые в квадратные скобки [].
12345678
letcomputedIdentifier='e';letv={a:'',// объявление идентификатора привычным способом,['b']:'',// объявление идентификатора с помощью строкового литерала.['c'+'d']:'',// объявление идентификатора с помощью выражения со строковыми литералами[computedIdentifier]:'',// объявление идентификатора при помощи вычисляемого идентификатора};
В первых двух случаях, вывод типов выводит ожидаемый тип, а в оставшихся тип с индексной сигнатурой.
123456
// let v1: { a: string; }letv1={a:'value',// Ok, привычный идентификатор};v1.b='value';// Error, в типе { a: string } не задекларирован идентификатор b
123456
// let v2: { ['a']: string; }letv2={['a']:'value',// Ok, строковый литерал};v2.b='value';// Error, в типе { ['a']: string } не задекларирован идентификатор b
12345678
letcomputedIdentifier:string='a';// let v3: { [x: string]: string }; - вывод типов выводит как тип с индексной сигнатурой...letv3={[computedIdentifier]:'value',// вычисляемое свойство};v3.b='value';// ... а это, в свою очередь, позволяет добавлять новое значение
123456
// let v4: { [x: string]: string }; - вывод типов выводит как тип с индексной сигнатурой...letv4={['a'+'b']:'value',// выражение со строковыми литералами};v4.b='value';// ... а это, в свою очередь, позволяет добавлять новое значение
Строгая проверка при обращении к динамическим ключам¶
Поскольку механизм, позволяющий определение индексной сигнатуры, не способен отслеживать идентификаторы (имена) полей, определенных динамически, такой подход не считается типобезопасным.
1 2 3 4 5 6 7 8 91011
typeT={[key:string]:number|string;};functionf(p:T){/** * Обращение к несуществующим полям */p.bad.toString();// Ok -> Ошибка времени исполненияp[Math.random()].toString();// Ok -> Ошибка времени исполнения}
Данная проблема решается с помощью флага --noUncheckedIndexedAccess, активирующего строгую проверку при обращении к динамическим членам объектных типов. Флаг --noUncheckedIndexedAccess ожидает в качестве значения true либо false. Активация механизма позволяет обращаться к динамическим членам только после подтверждения их наличия в объекте, a также совместно с такими операторами, как оператор опциональной последовательности ?. и опциональный оператор !..
typeT={[key:string]:number|string;};functionf(p:T){/** * Обращение к несуществующим полям */p.bad.toString();// Error -> TS2532: Object is possibly 'undefined'.p[Math.random()].toString();// Error -> TS2532: Object is possibly 'undefined'.// Проверка наличия поля badif('bad'inp){p.bad?.toString();// Ok}// Использование опционального оператораp[Math.random()]!.toString();// Ok -> ошибка во время выполненияp[Math.random()]?.toString();// Ok -> Ошибка не возникнет}
Не будет лишним упомянуть, что влияние данного механизма распространяется также и на массивы. В случае с массивом не получится избежать аналогичной ошибки при попытке обращения к его элементам при помощи индексной сигнатуры.
12345
functionf(array:string[]){for(leti=0;i<array.length;i++){array[i].toString();// Error -> TS2532: Object is possibly 'undefined'.}}
Запрет обращения к динамическим ключам через точечную нотацию¶
Поскольку к динамическим ключам можно обращаться как через точечную, так и скобочную нотацию, то может возникнуть потребность в разделении такого поведения. Для подобных случаев в TypeScript существует флаг --noPropertyAccessFromIndexSignature, установление которого в значение true запрещает обращение к динамическим членам с помощью точечной нотации.
typeSettings={env?:string[];// определение необязательного предопределенного поля[key:string]:any;// определение динамических полей};functionconfigurate(settings:Settings){//---------------------------// динамическое полеif(settings.envs){// Ошибка при активном флаге и Ok при неактивном}if(settings['envs']){// Ok при любом значении флага}//----------------------------// предопределенное полеif(settings.env){// Ok [1]}if(settings['env']){// Ok при любом значении флага}}
Тонкости совместимости индексной сигнатурой с необязательными полями¶
Объектные типы, определяющие строковую индексную сигнатуру, считаются совместимыми с объектными типами, имеющими необязательные поля.
1 2 3 4 5 6 7 8 9101112
/** * [0] поля определены как необязательные! */typeMagic={fire?:string[];water?:string[];};declareconstHERO_CONFIG:Magic;consthero:{[key:string]:string[]}=HERO_CONFIG;// Ok/**Ok*/
Но существует два неочевидных момента. При попытке инициализировать необязательные поля значением undefined возникнет ошибка.
1 2 3 4 5 6 7 8 91011121314151617
/** * [0] поля определены как необязательные! */typeMagic={fire?:string[];water?:string[];};declareconstHERO_CONFIG:Magic;/** * [1] Error -> * Type 'undefined' is not assignable to type 'string[]'. */consthero:{[key:string]:string[]}=HERO_CONFIG;hero['fire']=['fireball'];// Okhero['water']=undefined;// Error [1]
Кроме этого, ошибки не удастся избежать тогда, когда объектный тип, вместо опционального оператора, явно указывает принадлежность типа членов к типу undefined.
1 2 3 4 5 6 7 8 910111213141516
/** * [0] поля определены как обязательные! */typeMagic={fire:string[]|undefined;// [0]water:string[]|undefined;// [0]};declareconstHERO_CONFIG:Magic;/** * [1] Error -> * Type 'Magic' is not assignable to type '{ [key: string]: string[]; }'. */consthero:{[key:string]:string[]}=HERO_CONFIG;/**[1]*/
И кроме того, данные правила применимы исключительно к строковой индексной сигнатуре.
1 2 3 4 5 6 7 8 91011121314151617181920
/** * [0] ключи полей определены как индексы! */typePort={80?:string;// [0]88?:string;// [0]};declareconstSERVER_PORT:Port;/** * [2] Ключ индексной сигнатуры принадлежит к типу number. * * [1] Error -> * Type 'Port' is not assignable to type '{ [key: number]: string[]; }'. */constport:{[key:number]:string[];}=SERVER_PORT;/**[2] *//**[1] */