类型 'string | number' 的参数不能分配给类型 'never' 的参数



代码段和错误:

const methods = {
a(value: number) {},
b(value: string) {}
};
function callMethodWithArg(methodAndArg: { method: 'a'; arg: number; } | { method: 'b'; arg: string; }) {
methods[methodAndArg.method](methodAndArg.arg);
}

类型为"string | number"的参数不可分配给类型为"never"的参数。类型"string"不可分配给类型"never"。

看起来typescript不够智能,无法确定方法a只能用数字调用,方法b只能用字符串调用。

有什么关于如何正确键入的建议吗?

游乐场

这里的问题是,对于版本4.5之前的TypeScript,不支持我一直调用的"相关联";,如在microsoft/TypeScript#30581中所讨论的。正如您所说,编译器无法理解方法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!
}

f是函数的并集,arg是参数类型的并集。如果farg是不相关的(比如你从methodAndArg得到f,从同一类型的不同对象得到arg(,那么你当然不能用参数类型的并集调用函数的并集。可能fmethods.a,而bnumber。编译器分别跟踪farg类型,就好像它们是独立的一样。它不跟踪它们的


在TypeScript 4.5及以下版本中,我所知道的处理此问题的唯一方法是编写类型安全但冗余的代码,如以下所示:

function callMethodWithArg(methodAndArg: MethodAndArg) {
if (methodAndArg.method === "a") {
methods[methodAndArg.method](methodAndArg.arg);
} else {
methods[methodAndArg.method](methodAndArg.arg);
}
}

使用控制流分析将methodAndArg缩小到每个可能的子类型,然后编译器可以在每种情况下验证函数调用。

或者,您可以编写简洁但不安全的代码,比如:

function callMethodWithArg(methodAndArg: MethodAndArg) {
(methods[methodAndArg.method] as (value: number | string) => void)(
methodAndArg.arg);
}

使用类型断言来假装函数可以接受所有numberstring输入,即使实际上它只接受其中一个。这是不安全的,因为您可以传入Math.random()<0.5 ? "a" : 1而不是methodAndarg.arg,并且不会出现错误。


在TypeScript 4.6及更高版本中,您应该能够使用名为";分布式对象类型";如在microsoft/TypeScript#47109中介绍的。这个想法是提出一个映射对象类型,它表示从某个键名到函数参数的映射,然后使用这个映射对象类型来生成其他类型。编译器将能够";参见";只要事物以映射类型的形式正确表达,就可以实现相关性。

以下是您的示例:

// mapping type
interface ArgMap {
a: number;
b: string;
}

因此ArgMap是映射类型。从某种意义上说,它最简单地捕捉类型关系(例如,"anumber对应,bstring对应"(。现在我们可以构建您的其他类型:

// 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类型是ArgMap上的映射类型,我们将methods注释为该类型。此注释很重要,否则methodsmethodAndArg类型之间的链接将被断开。

下一篇:

// 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]

这里我们有一个通用的MethodAndArg<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

所以这是最好的!


我看到你真的想要一些工作量更少的东西,编译器可以";参见";相关性而不需要注释CCD_ 48。也许在未来的TypeScript版本中会发生这种情况,但目前我认为这是不可能的。

游乐场链接到代码

我不确定是否有更清洁的解决方案,但如果没有太多的情况,这将起作用:

const methods = {
a(value: number) {},
b(value: string) {}
};
function callMethodWithArg(methodAndArg: { method: 'a'; arg: number; } | { method: 'b'; arg: string; }) {
if (methodAndArg.method === 'a') {
// now it knows that method has to be 'a' and arg is a number
methods[methodAndArg.method](methodAndArg.arg)
} else if (methodAndArg.method === 'b') {
methods[methodAndArg.method](methodAndArg.arg)
}
}

游乐场链接

我的解决方案是使用类型谓词将方法强制转换为形状A或B(或者需要多少形状(

type MethodA = (value: number) => void
type MethodB = (value: string) => void
type ArgA = { method: 'a'; arg: number; }
type ArgB = { method: 'b'; arg: string; }
type Methods = {
[key: string]: MethodA | MethodB
}
const methods: Methods = {
a(value: number) { },
b(value: string) { }
};

function callMethodWithArg(methodAndArg: ArgA | ArgB) {
const method = methods[methodAndArg.method]
if (determinShape(methodAndArg)) {
(method as MethodA)(methodAndArg.arg)
} else {
(method as MethodB)(methodAndArg.arg)
}
}

/** Will cast methodAndArg as ArgA if true, else ArgB */
function determinShape(methodAndArg: ArgA | ArgB): methodAndArg is ArgA {
return Number.isFinite(methodAndArg.arg)
}

TS游乐场

相关内容

最新更新