'string | 类型的参数 number” 不可分配给“never”类型的参数

Luk*_*rus 5 typescript

片段和错误:

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只能用字符串调用方法。

有什么建议如何正确输入吗?

操场

jca*_*alz 6

这里的问题是,对于 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也是参数类型的联合。如果不知道farg是相关的(假设您f从相同类型的不同对象获得methodAndArgarg从相同类型的不同对象获得),那么您当然不能使用参数类型的并集来调用函数的并集。也许fmethods.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)

您使用类型断言假装该函数可以接受所有numberstring输入,即使实际上它只接受其中之一。这是不安全的,因为您可以传入代替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映射类型也是如此。从某种意义上说,它最简单地捕获了类型关系(例如,“anumberbstring”)。现在我们可以构建您的其他类型:

// 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 的未来版本中这可能会发生,但目前我认为这是不可能的。

Playground 代码链接