Mo Pro
Mo Pro

Reputation: 107

Merging type declarations with conditional types

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.

Playground Example

I expect ConditionalFn<string | number> to resolve to Fn<string> | Fn<number> and then further resolved to Fn<string | number>.


Follow up

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

Answers (1)

jcalz
jcalz

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

Related Questions