Я разрабатываю игру на 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;
ВОПРОСЫ: Почему третий подход работает, а первые два - нет? Что мне не хватает в том, как работает вывод типов? Есть ли другие шаблоны для достижения того, что мне нужно в подобных сценариях?
function requestChoice(request: ChoiceRequest<"monster">)
ошибки, как вы и ожидали - person Rubydesic   schedule 06.09.2020