I am using a library which has the following type defined:
type VoidableCallback<EventValue> = EventValue extends void ? () => void : (val: EventValue) => void;
The library exposes a function which returns the above type:
declare function fn<T>(): VoidableCallback<T>;
I want to use this function with a discriminated union:
type Action = { type: 'add'; n: number } | { type: 'multiply'; n: number };
const callback = fn<Action>();
callback({ type: 'add', n: 1 });
However Typescript (3.4.1) is giving me this error message:
Type '"add"' is not assignable to type '"add" & "multiply"'. Type '"add"' is not assignable to type '"multiply"'.ts(2322)
The expected type comes from property 'type' which is declared here on type '{ type: "add"; n: number; } & { type: "multiply"; n: number; }'
I don't understand why this is - it seems the the sum (union) type is being interpreted as a 'product' type.
If I change the type definition to:
type VoidableCallback<EventValue> = (val: EventValue) => void;
... Typescript doesn't complain. So it's something to do with the conditional type and union types.
If could understand what's going on here, then maybe I could make a PR to the library (rxjs-hooks).
This is caused by the distributive behavior of conditional types. Conditional types distribute over naked type parameters. This means if the type parameter contains a union, the conditional type will be applied to each member of the union and the result will be the union of all applications. So in your case we would get VoidableCallback<{ type: 'add'; n: number } | { type: 'multiply'; n: number }> = VoidableCallback<{ type: 'add'; n: number }> | VoidableCallback<{ type: 'multiply'; n: number }> = ((val: { type: 'add'; n: number }) => void) | ((val: { type: 'multiply'; n: number }) => void) You can read about this behavior here
The reason you get an error about the intersection is the way typescript deals with unions of function signatures, it basically requires that the parameters be compatible will all signatures in the union, so the parameter must be an intersection of all possible parameter types. You can read about this here
The simple solution is to disable the distribution for the conditional type. This is easily done by placing the type parameter in a tuple:
type VoidableCallback<EventValue> = [EventValue] extends [void] ? () => void : (val: EventValue) => void;
type Action = { type: 'add'; n: number } | { type: 'multiply'; n: number };
declare function fn<T>(): VoidableCallback<T>;
const callback = fn<Action>();
callback({ type: 'add', n: 1 }); //ok now
Playground link
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With