Home > Back-end >  Understanding TS Conditional types
Understanding TS Conditional types

Time:05-13

I'm trying to write generic variadic function but am falling at the first hurdle, here is a simplified example that fails and I can't understand why.

type TestType = {
    x: string,
    y: number,
}

type PickKeys<T extends object, K extends keyof T = keyof T> = T[K] extends string ? [K] : [never];

function testFn<T extends object>(...keys: PickKeys<T>) {}

testFn<TestType>("x"); // Argument of type 'string' is not assignable to parameter of type 'never'.

If I remove "y" from the TestType signature or change it to string it works but I thought this format would extract the keys where their value was of type string. I am clearly missing something fundamental. Any help would be appreciated.

CodePudding user response:

The problem here is that T[K] extends string ? [K] : [never] does not distribute over union members in K. It is not equivalent to (T["x"] extends string ? ["x"] : [never]) | (T["y"] extends string ? ["y"] : [never]).

That sort of automatic splitting-and-joining of unions only happens with a distributive conditional type, where the type being checked is the type parameter over whose union members you want to distribute. But T[K] is not a type parameter (T is a type parameter, and K is a type parameter, but T[K] is not... much like t might be a variable and k might be a variable but t[k] would not be) so T[K] extends ... ? ... : ... will not distribute over unions at all. And in any case you want to distribute over unions in K and not T[K].

So your PickKeys is therefore equivalent to

type PickKeys1<T extends object> = 
  T[keyof T] extends string ? [keyof T] : [never];

And if you plug in TestType you get

type PickKeysTestType = 
  TestType[keyof TestType] extends string ? [keyof TestType] : [never];
type PickKeysTestType1 = 
  (string | number) extends string ? ["x" | "y"] : [never];
type PickKeysTestType2 = 
  [never];

Since string | number is not a subtype of string, the conditional type evaluates to the false branch, which is just never. Oops.


If you want to distribute over unions in K, you can wrap the whole thing in a "no-op" distributive conditional type:

type PickKeys2<T extends object, K extends keyof T = keyof T> = 
  K extends unknown ? T[K] extends string ? [K] : [never] : never;

And then things work as desired:

function testFn<T extends object>(...keys: PickKeys2<T>) { }
testFn<TestType>("x"); // okay

There are other ways to implement something like PickKeys (I'm not sure why we need tuples here) but that's out of scope for the question as asked.

Playground link to code

  • Related