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
CodePudding user response:
Try this....
function fooFunc<T extends number | undefined>(param?: T){
// let x: T extends undefined ? null : T
let x = param ?? null as any
return x
}
CodePudding user response:
The missing type parameter is not being inferred as unknown here; by hovering over the function call, we can see that the type parameter is inferred as the upper bound number | undefined. Therefore the return type T extends undefined ? null : T, which is a distributive conditional type, resolves as number | null because the number part maps to number and the undefined part maps to null. So in the third example, the function's return type is number | null, which is not assignable to the type null, hence the error.
In this case - when you have a generic function with an optional parameter, but it only makes sense to be generic when the parameter is present - I think the most sensible workaround is to use function overloads. Then you can just directly specify what you want the return type to be when called with no argument, while still letting it be generic when called with an argument.
// overload for calling with no argument
function fooFunc(): null;
// overload for calling with one argument
function fooFunc<T extends number | undefined>(param: T): T extends undefined ? null : T;
// actual implementation
function fooFunc<T extends number | undefined>(param?: T) {
let x: T extends undefined ? null : T
x = param ?? null as any
return x
}
