I wrote a generic TypeScript function that ensures a function accepts either zero or two arguments, but not one. I.e. the second parameter is required if the first argument is passed in.
It looks like this:
function bar<T, P>(a?: undefined extends P ? never : T, b?: P) { }
bar('') // ERROR, as desired.
Initially, I had the conditional type the other way around, but that did not work:
function bar<T, P>(a?: P extends undefined ? never : T, b?: P) { }
bar('') // OK, which it should not be.
If the second argument, i.e. P
, is not passed in, then it is undefined
, which extends undefined
. Therefore, how come P extends undefined
does not work?
The order of the check in a conditional type matters in general, since X extends Y
and Y extends X
are two different things. This question presumes that P
is being inferred as undefined
, which would mean both checks were undefined extends undefined
, and that would indeed be surprising for there to be a difference. But P
is not being inferred as undefined
.
What happens with both
function foo<T, P>(a?: P extends undefined ? never : T, b?: P) { }
foo('') // error
function bar<T, P>(a?: undefined extends P ? never : T, b?: P) { }
bar('') // okay
is that no value of type P
is being passed in for b
, and P
cannot be inferred from a
. The inference for P
therefore completely fails. One might hope or expect that b
would be "implicitly" undefined
and therefore P
would be inferred from undefined
, but that's not what happens. Inference just fails.
When type argument inference fails for a generic type parameter, it falls back to its default type argument if one exists, or its constraint if no default exists. Unconstrained type parameters like P
have an implicit constraint of unknown
. So what actually happens in both cases is that P
is instantiated with unknown
.
And since unknown extends undefined
is false and undefined extends unknown
is true (unknown
is the top type in TS; it's a universal supertype) you get different behavior for foo()
and bar()
there.
If you want inference failure of P
to fall back to undefined
, you should make undefined
the default type argument:
function foo<T, P = undefined>(a?: P extends undefined ? never : T, b?: P) { }
foo(''); // error
function bar<T, P = undefined>(a?: undefined extends P ? never : T, b?: P) { }
bar(''); // error
Now P
is undefined
in both the above cases, and undefined extends undefined
is true in both cases so you get the same behavior.