这里的问题是 TypeScript 到 4.5 版本不支持我一直称之为“相关联合”的东西,正如 microsoft/TypeScript#30581 中所讨论的那样。正如您所说,编译器无法理解方法a 只能用number 调用,而方法b 只能用字符串调用。如果我们将单行分成几行来检查函数的类型及其参数,我们会看到每一行都是union type:
type MethodAndArg = { method: 'a'; arg: number; } | { method: 'b'; arg: string; }
function callMethodWithArg(methodAndArg: MethodAndArg) {
const f = methods[methodAndArg.method];
// const f: ((value: number) => void) | ((value: string) => void)
const arg = (methodAndArg.arg);
// const arg: string | number
f(arg) // error!
}
f 是函数的联合,arg 是参数类型的联合。如果不知道f 和arg 是相关的(假设您从methodAndArg 和arg 获得了来自同一类型的不同对象的f),那么您当然不能调用联合具有参数类型联合的函数。也许f 是methods.a 而b 是number。编译器分别跟踪f 和arg 的类型,就好像它们是独立的一样。它不跟踪他们的来源。
在 TypeScript 4.5 及更低版本中,我知道处理此问题的唯一方法是编写类型安全但冗余的代码,如下所示:
function callMethodWithArg(methodAndArg: MethodAndArg) {
if (methodAndArg.method === "a") {
methods[methodAndArg.method](methodAndArg.arg);
} else {
methods[methodAndArg.method](methodAndArg.arg);
}
}
您使用control flow analysis 将methodAndArg 缩小到每个可能的子类型,然后编译器可以在每种情况下验证函数调用。
或者,您可以编写简洁但不安全的代码,如下所示:
function callMethodWithArg(methodAndArg: MethodAndArg) {
(methods[methodAndArg.method] as (value: number | string) => void)(
methodAndArg.arg);
}
您使用type assertion 来假装该函数可以接受所有number 和string 输入,即使实际上它只会接受其中一个。这是不安全的,因为您可以传入 Math.random()<0.5 ? "a" : 1 而不是 methodAndarg.arg 并且不会出错。
在 TypeScript 4.6 及更高版本中,您应该能够使用 microsoft/TypeScript#47109 中介绍的称为“分布式对象类型”的东西。这个想法是想出一个映射对象类型,它表示从某个键名到函数参数的映射,然后使用这个映射对象类型来生成其他类型。只要根据映射类型正确表达事物,编译器就能够“看到”相关性。
您的示例如下所示:
// mapping type
interface ArgMap {
a: number;
b: string;
}
所以ArgMap 是映射类型。从某种意义上说,它最简单地捕获了类型关系(例如,“a 与 number 搭配,b 与 string 搭配”)。现在我们可以构建您的其他类型:
// map over ArgMap to get methods type
type Methods = { [K in keyof ArgMap]: (value: ArgMap[K]) => void }
const methods: Methods = {
a(value: number) { },
b(value: string) { }
};
Methods 类型是 mapped type 上的 ArgMap,我们 annotate methods 属于这种类型。这个注解很重要,否则methods和methodAndArg的类型之间的链接就会断开。
下一个:
// map over ArgMap and index into it to get generic MethodAndArg type
type MethodAndArg<K extends keyof ArgMap = keyof ArgMap> =
{ [P in K]: { method: P, arg: ArgMap[P] } }[K]
这里我们有一个genericMethodAndArg<K> 类型,其中K 默认为keyof ArgMap。如果你只写MethodAndArg,它与以前的联合类型相同,但如果你写MethodAndArg<"a">,它将只是a 方法/arg 对,MethodArg<"b"> 只是b 方法/arg 对.这就是我们需要的“分布式对象类型”。
最后,你的callMethodWithArg() 函数:
// make callMethodWithArg generic in the key type of ArgMap
function callMethodWithArg<K extends keyof Methods>(methodAndArg: MethodAndArg<K>) {
methods[methodAndArg.method](methodAndArg.arg); // okay
}
这个函数在K 中是通用的,methodAndArg 的类型是MethodAndarg<K>。相关函数调用编译没有问题,没有任何多余的 JS 代码或任何 TS 类型断言。这既是类型安全的,又能生成简洁的 JS 代码。当您调用 callMethodWithArg() 时,编译器将(在 TS4.6+ 中,记住)正确推断 K:
callMethodWithArg({ method: "a", arg: 123 }); // okay
// K inferred as "a"
callMethodWithArg({ method: "b", arg: "xyz" }); // okay
// K inferred as "b"
callMethodWithArg({ method: "a", arg: "xyz" }); // error!
// K inferred as "a", but arg is wrong
所以这已经差不多了!
我发现您确实想要工作量更少的东西,编译器可以“看到”相关性而无需注释 methods。也许在某些未来版本的 TypeScript 中可能会发生这种情况,但目前我认为这是不可能的。
Playground link to code