Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Understanding TS Conditional types

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.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>Solution :

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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading