Why isn't TS inferring the optional parameter in this function as undefined when it's omitted from the call?
function fooFunc<T extends number | undefined>(param?: T){ let x: T extends undefined ? null : T x = param ?? null as any return x } It works as expected if explicitly passed undefined but not if the argument is just omitted:
const fooResult1:number = fooFunc(3) const fooResult2:null = fooFunc(undefined) // <-- this works const fooResult3:null = fooFunc() // <-- this doesn't; it infers null | number, not null Things works as expected if I widen the constraint to include unknown, i.e., <T extends number | undefined | unknown>, but it's not a great solution as, in real code, unknown leads to other problems down the road.
What is going on here and is there a fix/workaround?
Update
It looks like the answer to they "why" part of my question is probably -- it just does. See this SO answer by @jcalz. So I guess my question is really is there any way around it, or do I just have to go down the path of debugging why unknown is giving me problems later on in my code?
Update 2(revised)
Here's my issue with unknown--if I change the function above to
function fooFunc<T extends number | undefined | unknown>(param?: T){ let x: T extends undefined ? null : T x = param ?? null as any return x } then it works, in the sense that it will correctly return a null type if called without a parameter, i.e, fooFunc(). but the problem is it will also now allow itself to be called with any arbitrary parameter, e.g., fooFunc("hello"), which is not the desired behavior.
now, you might say, well that's just because you can always add extra arguments in JS, but that's not what's going on here because the same issue happens if the generic is embedded in an object, i.e.,
function bazFunc<T extends number | undefined | unknown>(param: {a: number; b?: T}){ let x: T extends undefined ? null : T x = param.b ?? null as any return x } bazFunc({a:1, b:"hello"}) // <-- I don't want this Edit 3
I'm going to mark @kaya3's answer as accepted because I believe it accurately identifies the source of the problem, which is that the conditional type at the start of the function is distributive and so resolves to the union of the condition as applied to number and undefined. The proposed solution--using overloads--also seems good in many cases.
It did not work in my case because I needed return type inference. However, overriding the default conditional distributive behavior using [] did work. Thus, an alternate solution for the problem in the question is this, which works as I wanted:
function fooFunc<T extends number | undefined>(param?: T){ let x: [T] extends [number] ? T: null x = param ?? null as any return x } const fooResult1:number = fooFunc(3) const fooResult2:null = fooFunc(undefined) const fooResult3:null = fooFunc() By using the brackets to not distribute the conditional, it now resolves to just the actual value of T in the non-undefined case, which is what I wanted in the particular instance.