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.