I got an error while using ts generics, here is the simple code:
On the last line, ts reports the following error:
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);
I try to use switch to avoid errors:
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");
}
}
But it even worse:
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);
I noticed that vscode's type hint for task
is always Task<T>
instead of what I expected.
So, what should I do?
CodePudding user response:
The problem you are facing is that the compiler doesn't have great direct support for dealing with correlated union types, as described in microsoft/TypeScript#30581.
Your task
is of a union type:
declare var task: Task<"build"> | Task<"repair">;
So task
is either a Task<"build">
or a Task<"repair">
. And action_map[task.type].execute
is therefore also of a union type:
const execute = action_map[task.type].execute;
// const execute: ((task: Task<"build">) => void) | ((task: Task<"repair">) => void)
Which means execute
will either accept a Task<"build">
or it will accept a Task<"repair">
, but it will not accept both. Let's imagine we had another task:
declare var otherTask: Task<"build"> | Task<"repair">;
Clearly it would not be safe to use execute
to execute that task:
execute(otherTask); // error!
After all, otherTask
might be a Task<"build">
while execute
might be a ((task: Task<"repair">) => void)
. Well, unfortunately, the types are all the compiler can use to determine whether something is safe or not. Since execute(otherTask)
is unsafe, and task
is of the same union type as otherTask
, then execute(task)
is seen as unsafe for the same reason:
execute(task); // same error!
Of course we know that execute
will accept task
because they are correlated to each other. But that's because we're tracking the identity of the task
variable and not just the type. The compiler can't tell the difference between execute(otherTask)
and execute(task)
. So it worries that they might not be correlated.
The reason why the error mentions the intersection is because the only safe way for a union of functions to be called is to 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.
So that's why you're seeing an error.
As for fixing it, TypeScript 4.6 introduced some improvements specifically to address microsoft/TypeScript#30581; you can read about it in microsoft/TypeScript#47109.
The recommended approach is to make a generic function. If K extends TaskType
is a generic type parameter, the compiler will allow you to call a function of type (task: Task<K>)=>void
with a parameter of type Task<K>
. And the improvements in TypeScript 4.6 mean that the compiler will see action_map[task.type]
as being of 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
}
The SomeTask<T>
type is not required by the compiler, but it shows how you can get a union of task types easily. If TaskType
contained more union members, SomeTask
would also gain those members. And the body of genericExecute()
is seen as safe.
And luckily you can call genericExecute(task)
with no problem:
genericExecute(task); // okay
So that's the recommended approach to such correlated union types.