Reputation: 107
When a union of types are passed through a conditional type in TypeScript the returned value is not consistent when the conditional is removed.
When given a union of types typescript correctly merges the possible parameters into each parameter.
type Fn<T> = (arg: T) => void
Fn<string | number> // (arg: string | number) => void
However, when a conditional parameter is in the mix, the argument is resolved
type ConditionalFn<T> = T extends never ? Fn<T> : Fn<T>
ConditionalFn<string | number> // Fn<string> | Fn<number>
// That is equivalent to which is ((arg: string) => void | (arg: number) => void)
Usually this wouldn't be an issue. But when a function's type is set to the conditional kind, the argument's are all set to any
and a manual cast would be required.
I expect ConditionalFn<string | number>
to resolve to Fn<string> | Fn<number>
and then further resolved to Fn<string | number>
.
Why does the f
variable in the Improved behavior for calling union types
example require an intersection for the argument types?
I think that is root cause for my confusion. Since it forces the argument types to be any
if there isn't any intersection.
Upvotes: 2
Views: 738
Reputation: 327754
The conditional type you're using is distributive and expands ConditionalFn<string | number>
to Fn<string> | Fn<number>
as you expect. But Fn<string> | Fn<number>
is not assignable to Fn<string | number>
. Those are quite different types.
Fn<string | number>
is a very specific type of function; one which can accept both string
and number
arguments. It is a single function that says "I don't care if the argument is a string
or a number
; I will accept either of them".
Now a function of type Fn<string>
is only required to accept a string
, and function of type Fn<number>
is only required to accept a number
. And a function of type Fn<string> | Fn<number>
is one of those, we just don't know which one. It's quite an unspecific/vague function type. And therefore it's hard to actually call a function of such a type (prior to TS3.3 you couldn't call it at all without a type assertion), since the only way I could confidently pass it a parameter would be to give it something which is both a string
and a number
; that is, a string & number
. TypeScript 3.3 added support for calling such unions-of-functions with intersections, which is great... except that no values of type string & number
exist; it is equivalent to never
. So it is not possible to safely call a function of type ConditionalFn<string | number>
, and that is likely the cause of your issue.
Just to reiterate the difference between these types: The function (a: number) => console.log(2-a)
is a valid Fn<string> | Fn<number>
(since it is a Fn<number>
) but it is not a valid Fn<string | number>
(since it does not accept strings).
This does tend to confuse people because function types narrow/widen in the opposite direction from their argument types. This is called contravariance and TypeScript has supported it since version 2.6.
Okay, hope that helps; good luck!
Upvotes: 3