交集 'xxx & xxx' 被缩减为 'never' 因为 属性 'xxx' 在某些成分中有冲突的类型

The intersection 'xxx & xxx' was reduced to 'never' because property 'xxx' has conflicting types in some constituents

我在使用 ts 泛型时出错,这是简单的代码:

Typescript Playground

最后一行ts报如下错误:

error TS2345: Argument of type 'Task<"build"> | Task<"repair">' is not assignable to parameter of type 'never'.
  The intersection 'Task<"build"> & Task<"repair">' was reduced to 'never' because property 'type' has conflicting types in some constituents.
   
44 action_map[task.type].execute(task);

我尝试使用 switch 来避免错误:

function execute<T extends TaskType>(task: Task<T>) {
    switch (task.type) {
        case "build":
            // now type of `task` should be `Task<"build">`
            BuildAction.execute(task);
            break;
        case "repair":
            // now type of `task` should be `Task<"repair">`
            RepairAction.execute(task);
            break;
        default:
            // now type of `task` should be `Task<never>`
            console.log("Error");
    }
}

但更糟的是:

error TS2345: Argument of type 'Task<T>' is not assignable to parameter of type 'Task<"build">'.
  Type 'T' is not assignable to type '"build"'.
    Type 'keyof TMap' is not assignable to type '"build"'.
      Type '"repair"' is not assignable to type '"build"'.

50    BuildAction.execute(task);

我注意到 vscode 对 task 的类型提示总是 Task<T> 而不是我的预期。

那么,我该怎么办?

您面临的问题是编译器没有直接支持处理相关联合类型,如microsoft/TypeScript#30581.[=62 中所述=]

您的 task 属于 union type:

declare var task: Task<"build"> | Task<"repair">;

所以 task 要么是 Task<"build"> 要么是 Task<"repair">。因此 action_map[task.type].execute 也是联合类型:

const execute = action_map[task.type].execute;
// const execute: ((task: Task<"build">) => void) | ((task: Task<"repair">) => void)

这意味着 execute 要么接受 Task<"build">,要么接受 Task<"repair">,但不会同时接受两者。假设我们有另一个任务:

declare var otherTask: Task<"build"> | Task<"repair">;

显然使用 execute 执行该任务是不安全的:

execute(otherTask); // error!

毕竟,otherTask 可能是 Task<"build">,而 execute 可能是 ((task: Task<"repair">) => void)。好吧,不幸的是,编译器可以使用这些类型来确定某物是否安全。由于 execute(otherTask) 是不安全的,并且 taskotherTask 具有相同的联合类型,因此 execute(task) 被视为不安全的原因相同:

execute(task); // same error!

当然我们知道execute会接受task,因为它们彼此相关。但那是因为我们正在跟踪 task 变量的身份,而不仅仅是类型。编译器无法区分 execute(otherTask)execute(task) 之间的区别。所以它担心它们可能不相关。

错误提到交集的原因是因为调用函数联合的唯一安全方法是 call it with an intersection of the parameters. If execute accepts a Task<"build"> or a Task<"repair"> but we don't know which, the only safe thing to pass it would be something that's both a Task<"build"> and a Task<"repair">... hence a Task<"build"> & Task<"repair">. And of course such a thing is impossible; they have conflicting properties, so you get that message about the never type.

这就是您看到错误的原因。


至于修复它,TypeScript 4.6 引入了一些 improvements specifically to address microsoft/TypeScript#30581; you can read about it in microsoft/TypeScript#47109

推荐的方法是创建一个 generic 函数。如果 K extends TaskType 是泛型类型参数,编译器 允许您使用类型 Task<K> 的参数调用类型 (task: Task<K>)=>void 的函数。 TypeScript 4.6 的改进意味着编译器会将 action_map[task.type] 视为 (task: Task<K>)=>void:

类型
type SomeTask<T extends TaskType = TaskType> = { [K in T]: Task<K> }[T];

function genericExecute<K extends TaskType>(task: SomeTask<K>) {
    action_map[task.type].execute(task); // okay
}

编译器不需要 SomeTask<T> 类型,但它显示了如何轻松获得任务类型的联合。如果 TaskType 包含更多联合成员,SomeTask 也会获得这些成员。 genericExecute() 的 body 被认为是安全的。

幸运的是,您可以毫无问题地拨打 genericExecute(task):

genericExecute(task); // okay

因此,这是针对此类相关联合类型的推荐方法。

Playground link to code