I have 2 objects. Let's call them query and params. I have a function that has some logic inside but what it does mainly at the end is that it sets the value of query to params value.
type Params = {
a?: string;
b?: number;
c?: "purchase" | "payout";
};
type Query = {
d?: string;
e?: number;
f?: "purchase" | "payout";
};
const params: Params = {
a: "John",
b: 2,
c: "purchase",
};
const query: Query = {};
const processParam = (
queryKey: keyof Query,
paramKey: keyof Params
) => {
query[queryKey] = param[paramKey];
};
Needless to say that this is prone to type errors and won't even build unless you have primitive case where all your values in both objects have the same type. Ideally I want to have the following restriction:
Function accepts the key of the query, and then only allows keys of params such that the values in params are equal to values in query if you access them by respective passed keys. So if you pass such queryKey that the query[queryKey] is number, you can only pass such keys that param[paramKey] is number. And here where the fun begins, I just can't make it work.
Initially I thought it would take a simple generic. Something like that:
type PickKeysByValueType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
const processParam = <
QK extends keyof Query,
PK extends PickKeysByValueType<Params, Query[QK]>
>(
queryKey: QK,
paramKey: PK
) => {
query[queryKey] = params[paramKey];
};
However with that I get paramKey that can't even be used to index params ( I guess because it can possibly be never ). Is it possible to even do it without very complex typings? Seemed very easy before I actually tried.
This is currently not possible. It's a missing feature, described at microsoft/TypeScript#48992.
As you've noticed, you can define a type like PickKeysByValueType<T, V> (I usually call this KeysMatching<T, V>, and it's called KeysOfType<T, V> elsewhere) to compute the type you want, and this works quite well for specific T and V type arguments. but TypeScript is unable to reason about generic type arguments, like Query[QK] for generic QK. You know why you wrote PickKeysByValueType<T, V> but TypeScript cannot analyze its definition to see that T[PickKeysByValueType<T, V>] is always assignable to V no matter what V is. That would be higher order analysis TypeScript simply cannot perform.
The open feature request at microsoft/TypeScript#48992 is for an intrinsic KeysOfType<T, V> (meaning it's native to TypeScript and therefore intrinsic like Uppercase as per microsoft/TypeScript#40580 or NoInfer as per microsoft/TypeScript#56794). The intrinsic KeysOfType<T, V> would behave the same for specific types T and V, but TypeScript would recognize the validity of "T[KeysOfType<T, V>] extends V" for generic T and V. If that's ever implemented then you could just use it and things should probably work for you as-is (although there doesn't seem to be a reason for PK to be its own type parameter; you could just use KeysOfType<Params, Query[QK]>. That's an aside, though).
Until and unless it's implemented you have to work around it. By far the easiest workaround is just to use a type assertion inside the function:
type KeysOfType<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never;
}[keyof T];
const processParam = <QK extends keyof Query>(
queryKey: QK,
paramKey: KeysOfType<Params, Query[QK]>
) => {
const _params = params as Record<KeysOfType<Params, Query[QK]>, Query[QK]>; // just assert
query[queryKey] = _params[paramKey];
};
Here I'm asserting that Params is assignable to Record<KeysOfType<Params, Query[QK]>, Query[QK]> (which it should be; if T[KeysOfType<T, V>] extends V, then T extends Record<KeysofOfType<T, V>, V> should also be true). TypeScript can't follow that assertion without ms/TS#48992, so you just have to do it. Once you make that assertion you can then perform the assignment.
There are possibly other approaches leveraging the support for generic indexed accesses in microsoft/TypeScript#47109 that don't require type assertions. But I haven't found anything that doesn't require extra boilerplate code for every key in Query and so it's not really worthwhile. I won't belabor it here, but it's included in the playground link below for interested parties.