您可以为match()
编写正确的类型,但它不会以您希望的方式推断回调参数类型.这是TypeScrip中缺失的功能.您要么需要等待该功能实现(可能永远不会实现),要么以某种方式重构,要么放弃.
正确输入match()
如下所示:
const match = <V, C extends V[]>(
value: V,
cases: [...{ [I in keyof C]: Case<C[I]> }]
) => {
for (const [key, func] of cases) {
if (Array.isArray(key) ? key.includes(value) : key === value) {
return func(value as never);
}
}
};
在这里,cases
的类型不仅仅是Array<Case<V>>
,它不会试图跟踪数组中每个元素上V
的各个子类型,而且这对于子类型甚至是不正确的……例如,Case<"something">
不能分配给Case<string>
,因为虽然您可以将"something"
分配给string
,但由于function types are contravariant in their inputs的事实,您可以将(value: "something") => void
分配给(value: string) => void
.
相反,cases
的类型是mapped array type,而不是新的generic类型参数C
,新的generic类型参数C
是constrained至子类型V[]
.C
的每个元素用Case<⋯>
包装,以得到cases
的对应元素.因此,如果由["x", "y", "z"]
指定C
,则cases
的类型为[Case<"x">, Case<"y">, Case<"z">]
.
希望是如果你打电话给
const value: string = "whatever";
match(value, [
[ "something", (v) => {} ],
[ ["other", "thing"], (v) => { }]
]);
编译器会将V
推断为string
,这种情况确实会发生.它还会推断C
等于[ "something", "other" | "thing" ]
,不幸的是,not确实发生了.即使发生了did次,这些回调中的v
次中的contextual typing次也不会及时发生.
当函数参数相互依赖时,类型判断器很难推断出C
这样的泛型类型参数,同时推断出c
这样的函数参数的上下文类型.目前关于这一现象的公开问题一般是microsoft/TypeScript#47599个.特别是对于match()
,最适用的问题可能是microsoft/TypeScript#53018,因为我们试图从映射类型推断C
(从映射类型推断有时称为"反向"映射类型),然后在每个元素中上下文推断v
.
目前这还没有实现,所以上面的调用最终无法正确地推断出C
.它只是string[]
,这意味着v
的两个实例都是类型string
,这不是你想要的.
我不知道微软/TypeScrip#53018是否会实现,甚至不知道如果实现了,上面的代码是否会直接工作.目前,我们所能做的就是放弃或解决这个问题.
有很多可能的解决方法;最简单的一个可能是在对match()
的调用中使用助手函数来一次推断一个 case ,而不是一次推断所有 case .大概是这样的:
match(value, [
c("something", (v) => { }),
c(["other", "thing"], (v) => { })
]);
其中,c
被定义为
const c = <const T,>(
value: T | Array<T>,
handler: (value: T) => void
): Case<T> => [value, handler];
这是可行的;第一个v
被推断为"something"
,第二个v
被推断为"other" | "thing"
,因此,根据需要,对match()
的调用中的C
被推断为["something", "other"|"thing"]
.每次拨打c
只需要担心推断一件事,而不是一系列事情.
是的,这只是一种变通方法,但它相当轻量级;您所需要做的就是将[xxx,yyy]
替换为c(xxx,yyy)
,这只是一个额外的击键.是的,有一些认知开销,所以这就是为什么这是一个变通的办法,而不是直接的解决方案.
此解决方法或其他解决方法是否可接受取决于您的用例,并且由于问题不要求解决方法,因此这可能超出了问题的范围,所以我现在就不说了.
Playground link to code个