#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
}
}