Reputation: 1421
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.
Upvotes: 1
Views: 2537
Reputation: 51037
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
}
Upvotes: 1
Reputation: 1782
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
}
Upvotes: 0