UPDATE: Take a look at the revised solution below, inspired by @GarlefWegart's input.
I've been exploring the creation of generic typings for dynamic GraphQL query outcomes (mostly for fun, as I suspect similar solutions already exist).
I'm on the right track, but encountering a peculiar issue. You can find the complete code here in a testing environment, and replicated below.
The issue arises from attempting to index an object utilizing the keys of a derived object, which should work but is failing for unknown reasons. To clarify in the Result
declaration, I am unable to index T
using K
, despite having K
defined as a key of U
, and U
being recognized as a subset of the properties of T
. This implies that all keys of U
are inherently also keys of T
, making it supposedly safe to index T
with any key from U
. Yet, Typescript rejects this approach.
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 // exclude values for false keys
: U[K] extends true
? T[K] // keep the original type for true keys
// ^^vv All references to T[K] trigger errors
: T[K] extends Array<infer A>
? Array<Result<A, U[K]>> // Deliver an array of projection results when dealing with an array originally
: Result<T[K], U[K]>; // Otherwise treat it as an object and return the respective projection result
}
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: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="2a474f6a5f5904494547">[email protected]</a>",
address: {
country: {
code: "US"
}
},
docs: [
{
id: "1",
url: "https://abcde.com/docs/1"
},
{
id: "2",
url: "https://abcde.com/docs/2"
}
]
}
Your feedback and insights are welcomed.
Revision Mar 10, 2021
Following Garlef Wegart's suggestion provided earlier, a satisfactory resolution has been achieved. The updated code can be viewed here.
Kindly note, however, that it's quite delicate. It suits my specific requirements well since I'm structuring types for a GraphQL API where responses arrive as unknown
and are then cast based on input parameters. These types might not be universally applicable. For me, the focal point was not the assignment aspect but rather the utilization of the resulting type, which this solution adequately addresses. Good luck to others tackling similar challenges!
Additional Info: I have also made these types available as a compact Typescript package on GitHub (here). To use it, simply append
to your npmrc file and proceed with installing the package as usual.@kael-shipman:repository=https://npm.pkg.github.com/kael-shipman