Typescript: невозможно объявить дополнительные свойства для сопоставленных типов

#typescript

Вопрос:

Я столкнулся с проблемой при вводе объекта с помощью TypeScript

Я объявил тип, который используется некоторыми файлами приложения:

 type Category = "cat1"|"cat2"|"cat3"|"cat4"
 

Я использую Category для ввода объекта, и все работает нормально, используя:

 const obj: {
  [key in Category]: string
} = {---}
 

Но теперь я хотел бы добавить 2 ключа, которые не Category являются значениями для объекта.
Я думал, что это будет легко, введя объект следующим образом:

 const obj: {
  customKey1: string
  customKey2: string
  [key in Category]: string
} = {---}
 

Но вместо ожидаемой работы TS отправляет 3 ошибки: TS2464, TS1170 и TS2693

 A computed property name must be of type 'string', 'number', 'symbol' or 'any'. ts(2464)
'Category' only refers to a type, but is using as a value here. ts(2693)
A computed property name in a type template must refer to an expression whose type is a literal type or a 'unique symbol' type. ts(1170)
 

ОК… Почему?
Поскольку Category он используется для файлов, я не хочу редактировать его, добавляя эти пользовательские ключи, которые нужны только здесь.
Я решил эту проблему, поэтому, если кто-то еще сталкивается с этой проблемой, я даю решение ниже, но кто-нибудь знает, почему моя первая идея не может быть применена?
Я не понимаю, почему запись [key in Category]: string работает, если она одна, но выдает ошибки, если добавлен другой ключ.

Ответ №1:

{[K in XXX]: YYY} Синтаксис представляет собой сопоставленный тип, специальный тип объекта, который перебирает объединяющие элементы ключевого выражения типа XXX и использует параметр типа K для каждого из них внутри YYY выражения типа. (Обратите внимание, K представляет тип, а не значение ключа свойства, поэтому по соглашению мы используем там символы верхнего регистра). Вы можете использовать K внутри YYY выражения, чтобы каждый ключ K в XXX мог иметь другой тип значения, например {[K in "a" | "b"]: K} , эквивалентен {a: "a", b: "b"} . Синтаксис сопоставленного типа не является обобщаемым или расширяемым; вы не можете поместить туда другие свойства, например {[K in XXX]: YYY; somethingElse: string} , и вы не можете поместить их в interface объявления или сделать с ними что-либо еще. Это синтаксическая ошибка. В некотором смысле фигурные скобки { ... } являются частью синтаксиса для сопоставленных типов, и хотя они выглядят как фигурные скобки в других типах объектов, они не действуют как они.

Важно не путать этот синтаксис с похожим {[k: XXX]: YYY} синтаксисом для индексных подписей. Сопоставленные типы используют in ключевое слово, в то время как подписи индекса этого не делают, и требуют фиктивного идентификатора имени ключа ( k в приведенном здесь примере). Фиктивное имя ключа существует только внутри ключа и не может использоваться в YYY выражении, поэтому оно не позволяет вам назначать разные значения для разных ключей в XXX ; подписи индекса никоим образом не перебирают ключи внутри XXX . В качестве подписей индексные подписи могут использоваться в любом типе объекта наряду с другими свойствами, например {[k: XXX]: YYY; somethingElse: string} , при условии, что другие свойства не конфликтуют с индексной подписью. Они могут быть включены в interface объявления. Фигурные скобки не являются частью синтаксиса для подписей индексов.


Поэтому ваш вопрос: почему вы не можете добавить другие свойства к сопоставленному типу? почему фигурные скобки в сопоставленных типах работают не так, как в других типах объектов?

В microsoft / TypeScript # 13573 возникла проблема с этим точным вопросом. Однако, похоже, окончательного ответа нет. Это похоже на непредвиденный вариант использования. Некоторое время было возможно, что запрос на извлечение в microsoft / TypeScript # 26797 будет объединен с языком в рамках усилий по унификации сопоставленных типов и индексных подписей, но этого никогда не происходило. Соответствующий комментарий в microsoft / TypeScript # 45089 от члена команды TS объясняет, что на данный момент маловероятно, что здесь что-то изменится, поскольку возникают вопросы о том, как работать с возможно конфликтующими типами и обобщениями:

Смешивание отображаемых типов и объявлений свойств приводит к странным последствиям, которые элегантно решаются с помощью пересечений

Я не буду подробно описывать их здесь; вы можете посмотреть связанную проблему для получения дополнительной информации.


Итак, это ответ на вопрос «почему это так», насколько это возможно. Итак, что вы можете сделать вместо этого? В приведенном выше комментарии упоминаются пересечения; вы можете объединить свойства двух типов объектов вместе с помощью пересечений, поэтому {a: string} amp; {b: string} , по сути, это то же {a: string; b: string} самое, что и . Итак, один из способов написать свой тип объекта — это:

 type MyObjType =
  { [K in Category]: string } amp;
  { customKey1: string, customKey2: string };
 

Вы не можете напрямую использовать пересечения в качестве типов интерфейса, но вам разрешено расширять интерфейс другими именованными типами со статически известными ключами. Ваш {[K in Category]: string} имеет статически известные ключи, потому Category что не является универсальным, но это не именованный тип. Вы можете либо присвоить ему имя самостоятельно, а затем расширить его:

 type CategoryProps = { [K in Category]: string };
interface MyObjType extends CategoryProps {
  customKey1: string
  customKey2: string
}
 

Или вы можете использовать Record<K, V> тип утилиты и расширить его, не объявляя новое имя:

 interface MyObjType extends Record<Category, string> {
  customKey1: string
  customKey2: string
}
 

Наконец, вы всегда можете пойти в другом направлении и использовать сопоставленный тип для всех ваших ключей вместо того, чтобы пытаться добавлять их по отдельности:

 type MyObjType = 
  { [K in Category | "customKey1" | "customKey2"]: string };
 

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

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

1. Большое вам спасибо за этот полный ответ. Я думаю, ссылки, которыми вы поделились, помогут мне лучше понять TS. Читая сказанное, я предполагаю, что я действительно не различал сопоставленные типы и индексные подписи, я собираюсь подробно разобраться с этими понятиями. И спасибо за решения, первое — это то, которое я использовал в своем проекте. Я думаю, что я буду использовать второй с Record типом утилиты, более гибкий, чем 3-й (некоторые пользовательские ключи могут иметь разные типы).

2. Это окончательное руководство по предоставлению ответа SO. Объяснения ПОЧЕМУ и ссылки с github немного смягчили разочарование. Хотя я надеюсь, что команда TS поймет преимущества этого нового синтаксиса. Если пересечение работает, то, скорее всего, этот новый синтаксис может быть закодирован как трюк интерпретатора. Еще раз спасибо!

Ответ №2:

Для тех, кому не нужны объяснения, а только доступное решение, вот мое:

 type Keys = { [key in Category]: string }
interface Obj extends Keys {
  customKey1: string
  customKey2: string
}
const obj: Obj = {---}
 

Все работает нормально.