Как вывод типов работает с типами объединения (+ условными типами и универсальными шаблонами) в TypeScript?

Я разрабатываю игру на Angular и пытаюсь отделить представление от игровой логики. Для этого я создал отдельную UiController службу для обработки взаимодействия с пользователем и представления. Службы, связанные с игровой логикой, отправляют запросы к UiController всякий раз, когда что-то нужно показать или требуется действие пользователя.

Чтобы добиться этого как можно более аккуратно, я пытаюсь абстрагироваться от интерфейсов для взаимодействия с UiController. Одним из распространенных способов взаимодействия является выбор, когда игроки должны выбрать один из различных вариантов одной и той же категории. Это взаимодействие обрабатывается requestChoice() методом UiController, для которого требуется параметр типа ChoiceRequest. Поскольку существует множество различных категорий для выбора, этот тип должен содержать их все, и метод должен знать, как обращаться со всеми из них.

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

type HeroType = 'warrior' | 'rogue' | 'mage';
type MonsterType = 'goblin' | 'demon' | 'dragon';

Первый подход, который пришел мне на ум при создании ChoiceRequest, заключался в использовании обобщенных и условных типов:

type ChoiceType = 'hero' | 'monster';

type OptionsSet<T extends ChoiceType> = T extends 'hero'
  ? HeroType[]
  : T extends 'monster'
  ? MonsterType[]
  : never;

interface ChoiceRequest<T extends ChoiceType> {
  player: Player;
  type: T;
  options: OptionsSet<T>;
}

Это оказалось полезным при построении запросов на выбор, подобных этому, поскольку значения для type и элементов в options правильно предсказаны или отклонены:

const request: ChoiceRequest<'monster'> = {
  player: player2,
  type: 'monster',              // OK, any other value wrong
  options: ['demon', 'goblin']  // OK, any value not included in MonsterType wrong.
}

Однако вывод типа не работает должным образом, когда я пытаюсь заставить метод requestChoice() обрабатывать разные случаи:

public requestChoice<T extends ChoiceType>(request: ChoiceRequest<T>) {
  switch (request.type) {
    case 'a':             // OK, but should complain since values can only be 'hero' or 'monster'
      ...
    case 1:               // Here it complains, see below (*)
      ...
    ...
  }
}

(*) Тип «число» не сопоставим с типом «Т». 'T' может быть создан с произвольным типом, который не может быть связан с 'number'.

Раньше у меня неоднократно возникала эта проблема, но я не совсем понимаю, почему это происходит. Я подумал, что это как-то связано с условными типами, поэтому попробовал менее элегантный второй подход:

interface ChoiceMap {
  hero: HeroType[];
  monster: MonsterType[];
}

type ChoiceType = keyof ChoiceMap;

interface ChoiceRequest<T extends ChoiceType> {
  player: Player;
  type: T;
  options: ChoiceMap[T];
}

Однако этот подход работал точно так же, как и первый.

Единственный способ заставить эту работу работать так, как ожидалось, - это третий подход, построение ChoiceRequest явно как помеченное объединение, без универсальных или условных типов:

interface MonsterRequest {
  player: Player;
  type: 'monster';
  options: MonsterType[];
}

interface HeroRequest {
  player: Player;
  type: 'hero';
  options: HeroType[];
}

type ChoiceRequest = MonsterRequest | HeroRequest;

ВОПРОСЫ: Почему третий подход работает, а первые два - нет? Что мне не хватает в том, как работает вывод типов? Есть ли другие шаблоны для достижения того, что мне нужно в подобных сценариях?


person Dario Scattolini    schedule 05.09.2020    source источник
comment
Кажется, это просто сбой общих ограничений машинописного текста: function requestChoice(request: ChoiceRequest<"monster">) ошибки, как вы и ожидали   -  person Rubydesic    schedule 06.09.2020


Ответы (1)


Если вам не нужен T в возвращаемом типе, возможно, очень простой обходной путь:

function requestChoice(request: ChoiceRequest<ChoiceType>) {
  switch (request.type) {
    case 'a':             // Type '"a"' is not comparable to type ChoiceType
    case 1:               // Type '1' is not comparable to type ChoiceType
    case "hero": // fine
  }
}
person Rubydesic    schedule 05.09.2020