的主要问题是
const handlerConfigs: HandlerConfig[] = configs.map(({ eventName }) => {
return {
eventName,
handlerFactory: (context) => {
return (e) => { // error, implicit any
console.log('Did something cool!', context);
};
}
};
});
在回调函数中,eventName
的类型是union.如果你可以narrow eventName
到每个成员的unions 和判断它单独,你将有机会得到期望的行为:
configs.map(({ eventName }) => {
if (eventName === "onEnd") {
return {
eventName,
handlerFactory: (context) => {
return (e) => {
console.log('Did something cool!', context);
};
}
} satisfies HandlerConfig<"onEnd">;
} else return {
eventName,
handlerFactory: (context) => {
return (e) => {
console.log('Did something cool!', context);
};
}
} satisfies HandlerConfig<"onStart">;
});
但这是多余的.您可能认为编译器可以根据每个可能的缩小判断单个代码块,跟踪不同代码行中的联合成员之间的相关性,但这不是它的工作方式. (见https://twitter.com/SeaRyanC/status/1544414378383925250)
这是microsoft/TypeScript#30581的主题.
你能做的最简单的事情就是放弃编译器的精确判断,扩大内容或assert个内容,直到它停止抱怨.例如:
configs.map(({ eventName }) => {
const ret: HandlerConfig = {
eventName,
handlerFactory: (context) => {
return (e: StartEvent | EndEvent) => {
console.log('Did something cool!', context);
};
}
};
return ret;
});
这是因为(x: StartEvent | EndEvent) => void
满足两个事件处理程序.
另一方面,如果需要编译器遵循这样的逻辑,可以采用microsoft/TypeScript#47109中推荐的方法,在那里使用泛型而不是联合.
其思想是按照某种"基本"键-值映射类型编写操作,并将generic indexes into或基本类型写入mapped types,或在该类型上写入mapped types.
目标是希望通常看起来像联合的东西看起来像单个泛型类型. 下面是我在这个示例代码中的方法.基本类型如下:
interface EventMap {
onStart: StartEvent,
onEnd: EndEvent
}
(请注意,我呼叫了this EventMap
,并将your EventMap
重命名为HandlerMap
,因为它更具描述性):
type HandlerMap<K extends keyof EventMap = keyof EventMap> =
{ [P in K]: (event: EventMap[P]) => void }
类型HandlerMap
和以前一样(和你的EventMap
一样),但现在它是泛型的,所以HandlerMap<K>
只是带有键K
的对象的一部分.
我们也要改变HandlerConfig
:
type HandlerConfig<K extends keyof EventMap = keyof EventMap> = {
eventName: K;
handlerFactory: (context: AdditionalContext) => HandlerMap<K>[K];
};
HandlerMap<K>[K]
与(event: EventMap[K]) => void
相似,除了distributes over unions in 102. 也就是说,它是微软/TypeScript#47109中创造的distributive object type. 这假设如果K
是一个并,那么handlerFactory
也应该是一个并.如果你在乎这个,你可以写
type HandlerConfig<K extends keyof EventMap = keyof EventMap> = {
eventName: K;
handlerFactory: (context: AdditionalContext) => (event: EventMap[K]) => void;
};
但这最终只会模糊两个联盟成员之间的区别,并且您无法编写一个根据您获得的事件类型进行实际更改的版本.
不管怎样,从这里我们最终可以通过使map()
回调泛型来写出handlerConfigs
,说它将EventConfig<K>
转换为HandlerConfig<K>
:
const handlerConfigs: HandlerConfig[] = configs.map(
<K extends keyof EventMap>({ eventName }: EventConfig<K>): HandlerConfig<K> => {
return {
eventName,
handlerFactory: (context) => {
return (e) => {
console.log('Did something cool!', context);
};
}
};
});
现在,当你判断e
时,你会看到它是EventMap[K]
,你希望它是的类型(如果你愿意,你可以把它委托给依赖于eventName
的东西).
这样的重构值得吗?从这个用例来看,显然不是这样.只要用一些宽泛的东西来注释e
,然后继续前进是如此容易.但在某些情况下,这种重构是接近类型安全的唯一方法.
Playground link to code