TheDude
TheDude

Reputation: 198

Optional parameter of conditional type

I stumbled across a problem which I quite don't get. I found answers to similar questions but nothing which helped me to understand this.

I have the following function:

const one = <T extends boolean>(param?: T): T extends true ? string : number => {
  return (param ? '1' : 1) as T extends true ? string : number;
}

const foo = one(true); // type string
const bar = one(false); // type number
const baz = one(); // type string | number

The first two calls work as expected but I would expect that omitting the parameter would give me a value of type number and not string | number.

So my question is what causes this behavior and what is the accepted approach to solve this case? The following works as I would like it, but just causes more question marks in my head:

const one = <T extends boolean>(param?: T): false extends T ? number : string => {
  return (param ? '1' : 1) as false extends T ? number : string;
}

Upvotes: 1

Views: 1445

Answers (1)

Alex Wayne
Alex Wayne

Reputation: 187034

I'm not 100% sure why, but here's my hunch.

The compiler doesn't think it has enough info to decide which branch of the condition, so it gives up and says it could be either. You may think T is clearly a subtype of boolean | undefined, but you've also said T is a boolean, which can't be undefined.

When the argument is omitted params?: T has no type that would allow it to infer T. This is similar to this conditional type:

type A = any extends true ? 1 : 2 // 1 | 2

So when T used in a conditional the compiler has no idea what it could be, so it treats it as any, and returns both sides of the conditional.

I think the simplest fix is to provide a default type for T which will be used when the argument is omitted.

const one = <T extends boolean = false>(param?: T): T extends true ? string : number => {
  return (param ? '1' : 1) as T extends true ? string : number;
}

const baz = one(); // type number

Playground

I do agree the compiler should be able to figure out that T here is boolean | undefined and that does NOT extend true, but it's not perfect. Adding the default type clears up a lot of confusion for it.

If there's an issue with typescript here, it's that it lets you infer T from an optional argument without a default, which gives the unintended result because T isn't a type you can really use in that case.

Upvotes: 4

Related Questions