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