片段和错误:
const methods = {
a(value: number) {},
b(value: string) {}
};
function callMethodWithArg(methodAndArg: { method: 'a'; arg: number; } | { method: 'b'; arg: string; }) {
methods[methodAndArg.method](methodAndArg.arg);
}
Run Code Online (Sandbox Code Playgroud)
'string | 类型的参数 number” 不可分配给“never”类型的参数。类型“string”不可分配给类型“never”。
看起来打字稿不够智能,无法弄清楚a只能用数字调用方法,而b只能用字符串调用方法。
有什么建议如何正确输入吗?
这里的问题是,对于 TypeScript 4.5 版本之前的版本,不支持我所说的“相关联合”,如microsoft/TypeScript#30581中所述。正如您所说,编译器无法理解a只能使用 a 调用方法number并且b只能使用字符串调用方法。如果我们将单行分成几行来检查函数及其参数的类型,我们会看到每个都是联合类型:
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!
}
Run Code Online (Sandbox Code Playgroud)
这f是函数的联合,arg也是参数类型的联合。如果不知道f和arg是相关的(假设您f从相同类型的不同对象获得methodAndArg和arg从相同类型的不同对象获得),那么您当然不能使用参数类型的并集来调用函数的并集。也许f是methods.a并且b是一个number。编译器分别跟踪和 的类型,就好像它们是独立的一样。它不追踪它们的来源。farg
在 TypeScript 4.5 及更低版本中,我知道处理此问题的唯一方法是编写类型安全但冗余的代码,如下所示:
function callMethodWithArg(methodAndArg: MethodAndArg) {
if (methodAndArg.method === "a") {
methods[methodAndArg.method](methodAndArg.arg);
} else {
methods[methodAndArg.method](methodAndArg.arg);
}
}
Run Code Online (Sandbox Code Playgroud)
您可以使用控制流分析来缩小methodAndArg每个可能的子类型,然后编译器可以验证每种情况下的函数调用。
或者,您可以编写简洁但不安全的代码,如下所示:
function callMethodWithArg(methodAndArg: MethodAndArg) {
(methods[methodAndArg.method] as (value: number | string) => void)(
methodAndArg.arg);
}
Run Code Online (Sandbox Code Playgroud)
您使用类型断言假装该函数可以接受所有number和string输入,即使实际上它只接受其中之一。这是不安全的,因为您可以传入代替Math.random()<0.5 ? "a" : 1并且methodAndarg.arg不会出现错误。
在 TypeScript 4.6 及更高版本中,您应该能够使用microsoft/TypeScript#47109中介绍的“分布式对象类型” 。这个想法是提出一个映射对象类型,它表示从某个键名称到函数参数的映射,然后使用此映射对象类型来生成其他类型。只要用映射类型正确表达事物,编译器就能够“看到”相关性。
以下是您的示例的外观:
// mapping type
interface ArgMap {
a: number;
b: string;
}
Run Code Online (Sandbox Code Playgroud)
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) { }
};
Run Code Online (Sandbox Code Playgroud)
该Methods类型是上的映射类型ArgMap,我们将其注释 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]
Run Code Online (Sandbox Code Playgroud)
这里我们有一个通用 MethodAndArg<K>类型,K默认为keyof ArgMap. 如果你只写MethodAndArg它是与以前相同的联合类型,但如果你写MethodAndArg<"a">它只是a方法/参数对,并且MethodArg<"b">只是b方法/参数对。这就是我们需要的“分布式对象类型”。
最后,你的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
}
Run Code Online (Sandbox Code Playgroud)
该函数在 中是通用的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
Run Code Online (Sandbox Code Playgroud)
所以这已经是最好的了!
我发现您确实想要工作量更少的东西,编译器将能够“看到”相关性而不需要注释methods。也许在 TypeScript 的未来版本中这可能会发生,但目前我认为这是不可能的。
| 归档时间: |
|
| 查看次数: |
4357 次 |
| 最近记录: |