1

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?

Playground

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 

Refined playground

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.

2 Answers 2

1

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 } 

Playground Link

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for this idea. It may be the best answer for the question as posed. Unfortunately, I can't use overloads in my "real" example because I need return type inference.
But... the link to distributive conditions was really helpful. I didn't realize that behavior could be overridden with brackets. That solved my problem.
Marking this as accepted, but also see the alternative solution proposed in my edit to the question, i.e., overriding the distributive behavior of the conditional type.
Yes, if preventing distributivity gives you the desired result then that may be preferable to overloading, for sure.
0

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 } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.