Home > OS >  How to filter out a never type in generic parameter?
How to filter out a never type in generic parameter?

Time:11-03

Sorry for the uncertain title, I can't think of something better. Let me explain.

I have a complex TS type that looks like this: type Foobar <T extends object, K extends keyof T = keyof T> = ....

I need the K parameter to track the specific key type through the type definition and I suppose that the type definition itself does not play any role — because I give a minimal viable example below.

I want to see it anyway.

So, I have such a type and I need to check whether K is never or string:

type Foobar<T extends object, K extends keyof T = keyof T> = 
  K extends never 
    ? { __debug: 1 } 
    : K extends string
      ? { __debug: 2 }
      : { __debug: 3 }
;

let test1: Foobar<{ foo: unknown, bar: unknown }>; // { __debug: 2 }, as expected
let test2: Foobar<{}>;                             // { __debug: 1 } expected, got never
let test3: Foobar<object>;                         // { __debug: 1 } expected, got never

I expect that in the test2 and test3 cases the resulting type is { __debug: 1 }, but it's never. Why never? Where it came from? I have no never in my type definition at all.

More interesting, if I remove the K parameter, it works fine:

type Foobar<T extends object> = 
  keyof T extends never 
    ? { __debug: 1 } 
    : keyof T extends string
      ? { __debug: 2 }
      : { __debug: 3 }
;

let test1: Foobar<{ foo: unknown, bar: unknown }>; // { __debug: 2 }
let test2: Foobar<{}>;                             // { __debug: 1 }
let test3: Foobar<object>;                         // { __debug: 1 }

I'm absolutely confused about this behavior. Am I missing something or is it some kind of the compiler bug/restriction?

CodePudding user response:

The behavior you are seeing is caused by distribution. K is a naked generic type on the left side of a conditional. Therefore the compiler is trying to distribute the union in K over the conditional.

If K is never, this union is empty and there is "nothing" to distribute over. Therefore, the whole type returns never. You can imagine this being similar to an empty for-loop which never starts iterating leading to no result.

We can disable distribution by wrapping K inside a tuple.

type Foobar<T extends object, K extends keyof T = keyof T> = 
  [K] extends [never]
    ? { __debug: 1 } 
    : K extends string
      ? { __debug: 2 }
      : { __debug: 3 }

Which leads to the expected result.

let test1: Foobar<{ foo: unknown, bar: unknown }>; // { __debug: 2 } as expected
let test2: Foobar<{}>;                             // { __debug: 1 } as expected
let test3: Foobar<object>;                         // { __debug: 1 } as expected

Also note that in your second example, no distribution takes place as keyof T is not a naked generic type.


Playground

CodePudding user response:

You're never passing the second argument to type Foobar<T, K>

Writing it as a non-optional parameter:


type Foobar<T extends object, K extends keyof T> = 
  K extends keyof T
      ? { __debug: 2 }
      : { __debug: 3 }
;

let test1: Foobar<{ foo: unknown, bar: unknown }, "foo">; // { __debug: 2 }, as expected
let test2: Foobar<{}, "foo">;                             // { __debug: 3 } expected
let test3: Foobar<object, "bar">;                         // { __debug: 3 } expected

In your code, you allow K to be optional, when nothing is passed, the code freaks out a little, and goes "well this doesn't exist" and defaults to passing never

Allowing it to be optional again (as in your code, and below), we get that erroneous behaviour.

Notice how test21 and test22 behave the same:

type Foobar<T extends object, K = keyof T> = 
  K extends keyof T
      ? { __debug: 2 }
      : { __debug: 3 }
;

let test1: Foobar<{ foo: unknown, bar: unknown }, "foo">; // { __debug: 2 }, as expected
let test2: Foobar<{}, "foo">;                             // { __debug: 3 } expected
let test21: Foobar<{}>;                                   // never expected, because we didnt pass anything
let test22: Foobar<{}, never>;                            // never expected, because we didnt pass anything useful
let test23: Foobar<{}, unknown>;                          // { __debug: 3 } expected because K not a key of T
let test3: Foobar<object, "bar">;                         // { __debug: 3 } expected because K not a key of T

Edit

It's to do with how TypeScript unionises its types...

So I found another question that posed this, here's their answer.

The upshot is:

Conditional types distribute over naked type parameters. This means that the conditional type gets applied to each member of the union. never is seen as the empty union. So the conditional type never gets applied (since there are no members in the union to apply it to) resulting in the never type.

You get never because that's the fallback type.

The link in the above answer is deprecated now. Here's the new link on the same topic.

  • Related