[英]How does type inference work with union types (+conditional types and generics) in TypeScript?
I am developing a game in Angular, and trying to decouple presentation from game logic.我正在用 Angular 开发一个游戏,并试图将演示与游戏逻辑分离。 In order to achieve this I have built a separate UiController
service to handle user interactions and presentation.为了实现这一点,我构建了一个单独的UiController
服务来处理用户交互和演示。 Services related to game logic make requests to UiController
whenever something needs to be shown or a user action is needed.每当需要显示某些内容或需要用户操作时,与游戏逻辑相关的服务都会向UiController
发出请求。
In order to achieve this as neatly as possible, I'm trying to abstract away interfaces to interact with UiController
.为了尽可能巧妙地实现这一点,我试图抽象出接口以与UiController
交互。 One common interaction is choice , used when players must choose one among different options of the same category.一种常见的交互是选择,当玩家必须在同一类别的不同选项中选择一个时使用。 That interaction is handled by a requestChoice()
method of UiController
, which requires a parameter of ChoiceRequest
type.该交互由UiController
的requestChoice()
方法处理,该方法需要ChoiceRequest
类型的参数。 Since there are many different categories for choices, this type must contain all of them and the method must know how to deal with all of them.由于有许多不同的类别可供选择,因此该类型必须包含所有这些类别,并且该方法必须知道如何处理所有这些类别。
For instance, users may be required to choose monsters or heroes.例如,用户可能需要选择怪物或英雄。 I use literal types to refer to options in choices:我使用文字类型来指代选择中的选项:
type HeroType = 'warrior' | 'rogue' | 'mage';
type MonsterType = 'goblin' | 'demon' | 'dragon';
The first approach that occured to me to build ChoiceRequest
was to use generics and conditional types:我ChoiceRequest
的第一种构建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>;
}
This proved useful when building choice requests like this, since the values for type
and items in options
are correctly predicted or rejected:这在构建这样的选择请求时被证明是有用的,因为options
中type
和项目的值被正确预测或拒绝:
const request: ChoiceRequest<'monster'> = {
player: player2,
type: 'monster', // OK, any other value wrong
options: ['demon', 'goblin'] // OK, any value not included in MonsterType wrong.
}
However, type inference does not work as expected when I try to make requestChoice()
method handle different cases:但是,当我尝试使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 (*)
...
...
}
}
(*) Type 'number' is not comparable to type 'T'. (*) 类型 'number' 不能与类型 'T' 进行比较。 'T' could be instantiated with an arbitrary type which could be unrelated to 'number'. 'T' 可以用与 'number' 无关的任意类型实例化。
I have had this problem repeatedly before, but I don't fully understand why this happens.我以前反复遇到过这个问题,但我不完全明白为什么会发生这种情况。 I thought it had something to do with conditional types, so I then tried a less elegant second approach :我认为它与条件类型有关,所以我尝试了一种不太优雅的第二种方法:
interface ChoiceMap {
hero: HeroType[];
monster: MonsterType[];
}
type ChoiceType = keyof ChoiceMap;
interface ChoiceRequest<T extends ChoiceType> {
player: Player;
type: T;
options: ChoiceMap[T];
}
However, this approach worked exactly as the first.但是,这种方法与第一种方法完全一样。
The only way to make this work as expected was a third approach , building ChoiceRequest
explicitly as a tagged union, without generics or conditional types:使这项工作按预期进行的唯一方法是第三种方法,将ChoiceRequest
显式构建为标记联合,没有泛型或条件类型:
interface MonsterRequest {
player: Player;
type: 'monster';
options: MonsterType[];
}
interface HeroRequest {
player: Player;
type: 'hero';
options: HeroType[];
}
type ChoiceRequest = MonsterRequest | HeroRequest;
QUESTIONS: Why does the third approach work and the first two don't?问题:为什么第三种方法有效而前两种方法无效? What I am missing about how type inference works?关于类型推断的工作原理,我缺少什么? Are there other patterns to achieve what I need in scenarios like this?在这样的场景中,是否还有其他模式可以实现我所需要的?
If you don't need T in the return type a very simple workaround is probably:如果您不需要返回类型中的 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
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.