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

#typescript #generics #type-inference #union-types #conditional-types

#typescript #обобщения #вывод типа #объединяющие типы #условные типы

Вопрос:

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

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

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

 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 (*)
      ...
    ...
  }
}
  

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

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

 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;
  

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

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

1. Похоже, это просто сбой общих ограничений typescript: function requestChoice(request: ChoiceRequest<"monster">) ошибки, как вы и ожидали

Ответ №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
  }
}