Что такое индексируемые типы в typescript?

#typescript

#typescript

Вопрос:

Я читал индексируемые типы, и шаблон «словарь» меня смутил:

 interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}
 
 interface StringArray {
  [index: number]: string;
}
 

В чем разница между ними?
Как вы вообще инициализируете переменную с типом NumberOrStringDictionary ? И почему NumberOrStringDictionary поля ( length и name ) зависят от поля, подписанного индексом?

Чем NumberOrStringDictionary отличается от этого:

 interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}
 

Ответ №1:

Этот тип:

 interface NumberOrStringDictionary {
    [index: string]: number | string;
    length: number;
    name: string;
}
 

подразумевает, что значение типа NumberOrStringDictionary должно содержать свойство с именем length типа number свойство с именем name типа string . Кроме того, значение этого типа может содержать свойства с любым string именем, если значения таких свойств можно присвоить string | number .

Несколько моментов для уточнения:

  • Подписи индекса не могут конфликтовать с любыми другими ключами. Например, length свойство имеет тип number , поэтому оно совместимо с сигнатурой индекса ( number может быть присвоено number | string ). И это name свойство также совместимо. Если вы попытаетесь добавить свойство какого-либо несовместимого типа, вы получите сообщение об ошибке:
     interface Oops extends NumberOrStringDictionary {
        acceptable: boolean; // error!
        //~~~~~~~~ <-- boolean not assignable to string | number
    }
     

    Если у меня есть ключ типа string и я считываю свойство по этому ключу из значения type NumberOrStringDictionary , каков тип этого свойства? Это так number | string . Теперь представьте, что произошло бы, если бы вам разрешили добавить несовместимое свойство (например acceptable: boolean ). В этом случае это больше не будет правдой. Если ключ, который я выбираю, оказывается, есть "acceptable" , то boolean выходит a, которого нет number | string , значит, что-то не так. Чтобы быть в безопасности, вы должны ожидать number | string | boolean .

    Вы можете думать о свойствах неиндексной подписи как о частных случаях индексной подписи, когда вы знаете, что ключ свойства определенно существует и что значение имеет какой-то, возможно, более конкретный тип. Известные свойства следует рассматривать как частные / особые случаи индексатора. «Все свойства этого объекта имеют тип string | number . В частности, length свойство этого объекта имеет тип number , а name свойство этого объекта имеет тип string «. Это все равно, что сказать: «Все мои домашние животные — собаки. Фидо здесь — пудель. » Несовместимость будет выглядеть так: «Все мои домашние животные — собаки. Фидо здесь — попугай «. Вы можете так сказать, но это не согласуется. Возможно, вы думаете об индексных сигнатурах как о случае «по умолчанию» или «все остальное» или «остальное» («Мои домашние животные — все собаки, кроме этого попугая»), но это не то, что это означает в TypeScript. Существует открытая проблема, microsoft / TypeScript # 17867 запрашивает такую конструкцию в языке, но ее еще нет (есть обходные пути).

  • Подпись индекса обычно не означает, что будут отображаться свойства со всеми string ключами. Вам разрешено указывать столько или столько таких свойств, сколько вы хотите. Поэтому возможно, что при проверке свойства type NumberOrStringDictionary оно будет отсутствовать. Когда вы прочитаете такое отсутствующее свойство, вы получите значение типа undefined , а не of string | number , но компилятор сделает вид, что это так string | number . В TypeScript 4.1 введен флаг компилятора для обработки undefined как возможного результата чтения любого свойства подписи индекса, но по умолчанию он не включен.

Возможно, было бы полезно просмотреть некоторые примеры и посмотреть, что принимается, а что отклоняется компилятором. Вот допустимое назначение:

 const valid: NumberOrStringDictionary = {
    length: 1,
    name: "bob",
    someOtherKey: 123,
    someOtherKey2: "hey"
};
 

Реквизит length и name свойства находятся там, а дополнительные свойства соответствуют подписи индекса. Это также допустимо:

 const alsoValid: NumberOrStringDictionary = {
    length: 1,
    name: "bob",
};
 

потому что вам не нужно добавлять какие-либо такие свойства индексной подписи. Теперь об ошибках:

 const invalid1: NumberOrStringDictionary = { // error!
    //~~~~~~~~ <- property "name" is missing
    length: 1,
    someOtherKey: 123,
    someOtherKey2: "key"
};
 

Вы не можете пропустить обязательное свойство. Также:

 const invalid2: NumberOrStringDictionary = {
    length: "fred", // error!
    //~~~~ <-- string is not assignable to number
    name: "bob",
    someOtherKey: 123,
    someOtherKey2: "key"
};
 

Вы не можете указать неправильный тип требуемого свойства. И, наконец,:

 const invalid3: NumberOrStringDictionary = {
    length: 1,
    name: "bob",
    someOtherKey: 123,
    someOtherKey2: true // error!
    //~~~~~~~~~~~ <-- boolean is not string | number
};
 

Вы не можете добавить свойство, которое конфликтует с сигнатурой индекса.


Игровая площадка ссылка на код

Комментарии:

1. Я понимаю все, кроме того, почему [index: string] тип s должен быть совместим с типами других полей? Это просто так, как это реализовано в typescript, или я что-то здесь упускаю? Не могли бы вы, пожалуйста, прояснить это? ts interface NumberDictionary { [index: string]: number; length: number; // ok, length is a number name: string; // error, the type of 'name' is not a subtype of the indexer } Почему [index: string] свойство должно заботиться о типе name свойства?

2. Если у меня есть ключ типа string , и я считываю свойство по этому ключу из значения типа {[index: string]: number; length: number} , каков тип этого свойства? Это так number . Теперь представьте, что произошло бы, если бы вам разрешили добавить несовместимое свойство (например name: string ). В этом случае это больше не будет правдой. Если я выбираю ключ "name" , то string выходит a, которого нет number , значит, что-то не так. TypeScript обеспечивает согласованность здесь. Имеет ли это смысл?

3. Известные свойства следует рассматривать как частные / особые случаи индексатора. «Все свойства этого объекта имеют тип string | number . В частности , length свойство этого объекта имеет тип number , а name свойство этого объекта имеет тип string «. Это все равно, что сказать: «Все мои домашние животные — собаки. Фидо здесь — пудель. » Несовместимость будет выглядеть так: «Все мои домашние животные — собаки. Фидо здесь — попугай «. Вы можете так сказать , но это не согласуется.

4. Я думаю, вы можете думать об индексной подписи как о случае «по умолчанию» или «все остальное» или «остальное» («Мои домашние животные — все собаки, кроме этого попугая»), но это не то, что это означает в TypeScript. Существует открытая проблема, microsoft / TypeScript # 17867 запрашивает такую конструкцию в языке, но ее еще нет (есть обходные пути).

5. The known properties should be thought of as particular/special cases of the indexer. У меня был момент — Ага, когда я читал это. Спасибо.

Ответ №2:

Объекты в JS можно рассматривать как словарь. Они могут принимать произвольные ключи, которые сопоставляются с соответствующими значениями. Эта двойственность объекта и словаря также видна в синтаксисе:

 const o = {key: 1};
console.log(o['key']);
console.log(o.key)
 

Свойства можно добавлять и удалять во время выполнения.

В общем, большинство объектов имеют постоянный, известный набор свойств, и вы моделируете их как обычные интерфейсы в TypeScript.

Вам нужно использовать индексированный тип, если вы хотите моделировать объекты, которые следуют правилам JS

  • может содержать любое свойство
  • может быть изменен во время выполнения путем добавления и удаления свойств

Обратите внимание, что существует 2 вида индексов

  • число — вы можете индексировать только через число (что означает, что вы используете их как массив)
  • строка — вы можете индексировать с помощью любой строки (вы используете их как dict)

В вашем примере:

 interface StringArray {
  [index: number]: string; // number index
}
 
 interface NumberOrStringDictionary {
  [index: string]: number | string;   // string index
  length: number; 
  name: string; 
}
 

Переменные этих интерфейсов определяются следующим образом:

 const myArray: StringArray = ["Bob", "Fred"];
console.log(myArray[0])

const d: NumberOrStringDictionary = {
  a: 1,
  b: 2
  length: 2,
  name: 'myDict'
}

// You can add and remove properties in runtime
d.c = 1;
console.log(d['c'])
delete d.b;
console.log(d['c'])
 

Индексированные типы позволяют указывать типы значений свойств.

 const d1: NumberOrStringDictionary = {
  a: true,  // error: Type 'boolean' is not assignable to type 'string | number'.
  b: {},    // error: Type '{}' is not assignable to type 'string | number'.
  length: 1,
  name: 'myDict'
}
 

Как описано в ссылке из исходного сообщения, если вы добавляете дополнительные свойства к типу, индексируемому строками, типы этих свойств должны быть совместимы с типом, указанным для индекса.