有没有办法在编译时缩小类型的范围



我试图使用条件类型定义来缩小联合类型的范围,但无论我怎么努力,我都无法理解为什么这个代码失败:

const makeEventA = (value: number) => ({ _tag: "a" as const, payload: value })
type EventA = ReturnType<typeof makeEventA>
const makeEventB = (value: string) => ({ _tag: "b" as const, payload: value })
type EventB = ReturnType<typeof makeEventB>
type AnyEvent = EventA | EventB;
type AnyEventTag = AnyEvent["_tag"];
type FindByTag<A, B extends AnyEventTag> = A extends { _tag: B } ? A : never;
type Mapping = {
[K in AnyEventTag]?: (ev: FindByTag<AnyEvent, K>) => Promise<void>;
};
const execute = (m: Mapping) => (ev: AnyEvent) => {
const handler = m[ev._tag];
if (handler) {
handler(ev);
}
};

我的呼叫handler(ev)生成以下内容:

Argument of type 'AnyEvent' is not assignable to parameter of type 'never'.
The intersection 'EventA & EventB' was reduced to 'never' because property '_tag' has conflicting types in some constituents.
Type 'EventA' is not assignable to type 'never'.

这里有一个指向沙盒的链接,代码为=>https://codesandbox.io/embed/dank-dawn-ilcxt?fontsize=14&隐藏导航=1&主题=黑暗&view=编辑器

有人能向我解释为什么我错了吗?

这里的根本问题是编译器不理解handler的类型与ev的类型相关的事实;它将它们视为独立的联合类型。有关详细信息,请参阅microsoft/TypeScript#30581。所以CCD_ 4是一个错误;交叉相关的";情况。编译器在面对handler(ev)时不会执行您所做的操作,并针对ev的每个可能的_tag属性对其进行多次分析。如果它真的这样做了,就不会有问题;实际上,您可以通过实际写出冗余代码来强制编译器进行这样的分析:

const executeRedundant = (m: Mapping) => (ev: AnyEvent) => {
if (ev._tag === "a") {
const handler = m[ev._tag];
if (handler) {
handler(ev); // okay
}
} else {
const handler = m[ev._tag];
if (handler) {
handler(ev); // okay
}
}
};

这是类型安全,但blecch。重构到冗余代码不会扩展。


如果要为类型安全性重构代码,可以这样做,使其在_tag属性的类型中具有泛型。您可以使用在microsoft/TypeScript#47109中提出的方法,其中您将AnyEvent区分的并集表示为";"分布式对象类型":

type TagPayloadMap = {
a: number;
b: string;
}
type AnyEventTag = keyof TagPayloadMap
type AnyEvent<K extends AnyEventTag = AnyEventTag> =
{ [P in K]: { _tag: P, payload: TagPayloadMap[P] } }[K]
const makeEvent = <K extends AnyEventTag>(tag: K) => (value: TagPayloadMap[K]): AnyEvent<K> => ({ _tag: tag, payload: value });
const makeEventA = makeEvent("a");
const makeEventB = makeEvent("b");
type EventA = AnyEvent<"a">;
type EventB = AnyEvent<"b">;
type Mapping<K extends AnyEventTag = AnyEventTag> = {
[P in K]?: (ev: AnyEvent<P>) => Promise<void>;
};
const execute = <K extends AnyEventTag>(m: Mapping<K>) => (ev: AnyEvent<K>) => {
const handler = m[ev._tag];
if (handler) {
handler(ev); // okay
}
};

这一切现在都很好(即使在TS4.5中,尽管在TS4.6+中调用execute()显然会更容易(,编译器认为handler(ev)是可以接受的,因为它在K中都是通用的。类型AnyEventMapping等都是相同的,但取决于TagPayloadMap类型。(您的FindByTag功能包含在新的AnyEvent类型中;例如,您可以编写AnyEvent<'a'>而不是FindByTag<AnyEvent, 'a'>。(


如果您因为依赖于当前结构而不打算重构代码,那么您将不得不放弃类型安全。编译器就是看不到你所做的是安全的。在这种情况下,可以使用类型断言来告诉编译器不要太担心类型。它将维护类型安全的负担从编译器(这不取决于任务(转移到您身上。所以,三次检查你做得对,然后断言。

例如:

const executeAssert = (m: Mapping) => (ev: AnyEvent) => {
const handler = m[ev._tag];
if (handler) {
(handler as (value: AnyEvent) => Promise<void>)(ev);
}
};

这里我们说handler可以被看作是一个取AnyEvent的函数;这并不是完全正确的,但这是一个相当无害的谎言,它让编译器相信调用handler(ev)是可以的。再说一遍,这是不安全的;您可以传入ev以外的东西,比如随机的AnyEvent,编译器不会眨眼。所以你需要小心。


所以这些是选项:重构为冗余;重构为泛型;或者使用类型断言来抑制错误。

游乐场链接到代码

问题是handler(忽略undefined,即在if块内(,是映射EventA => Void<Promise>或映射EventB => Void<Promise>,而输入ev是类型EventAEventB。因此,从编译器的角度来看,例如,handlerEventA => Promise<Void>类型,而evEventB类型,这两种类型是不兼容的。类型FindByTag不基于handler(ev)1是什么而将AnyEvent具体化为实际的EventAEventB

最新更新