NOTE: See edit below for final solution based on @GarlefWegart's response.
I'm trying to write generic typings for dynamic GraphQL query results (more for fun, since I'm sure these probably exist somewhere already).
I'm very close, but have tapped out on a weird problem. The full code is here in a playground, and reproduced below.
The problem is centering around indexing an object using the keys of a derived object, which should work but for some reason is failing. Notice in the Result definition that I can't index T using K, even though K is defined as a key of U, and U is defined as a subset of the properties of T. This means that all of the keys of U are necessarily also keys of T, so it should be safe to index T with any key of U. However, Typescript refuses to do so.
type SimpleValue = null | string | number | boolean;
type SimpleObject = { [k: string]: SimpleValue | SimpleObject | Array<SimpleValue> | Array<SimpleObject> };
type Projection<T extends SimpleObject> = {
[K in keyof T]?:
T[K] extends SimpleObject
? Projection<T[K]>
: T[K] extends Array<infer A>
? A extends SimpleObject
? Projection<A>
: boolean
: boolean;
};
type Result<T extends SimpleObject, U extends Projection<T>> = {
[K in keyof U]:
U[K] extends false
? never // don't return values for false keys
: U[K] extends true
? T[K] // return the original type for true keys
// ^^vv All references to T[K] throw errors
: T[K] extends Array<infer A>
? Array<Result<A, U[K]>> // Return an array of projection results when the original was an array
: Result<T[K], U[K]>; // Else it's an object, so return the projection result for it
}
type User = {
id: string;
email: string;
approved: string;
address: {
street1: string;
city: string;
state: string;
country: {
code: string;
allowed: boolean;
}
};
docs: Array<{
id: string;
url: string;
approved: boolean;
}>
}
const projection: Projection<User> = {
id: false,
email: true,
address: {
country: {
code: true
}
},
docs: {
id: true,
url: true
}
}
const result: Result<User, typeof projection> = {
email: "[email protected]",
address: {
country: {
code: "US"
}
},
docs: [
{
id: "1",
url: "https://abcde.com/docs/1"
},
{
id: "2",
url: "https://abcde.com/docs/2"
}
]
}
Any insight is appreciated.
Edit Mar 10, 2021
I was able to get to an acceptable solution based on Garlef Wegart's response below. See the code here.
Note, however, that it's very finicky. It fits my use-case fine because I'm typing a GraphQL API, so the response comes in as unknown over the wire and is then cast to the value inferred by the input parameters, but these types may not work in other situations. The important part for me was not the assignment part, but the consumption of the resulting type, and this solution does work for that. Best of luck to anyone else trying for this!
Second Note: I've also published these types as a small Typescript package on github (here). To use it, just add @kael-shipman:repository=https://npm.pkg.github.com/kael-shipman to your npmrc somewhere and then install the package as normal.
Array<Projection<A>>in the definition ofProjection?