So I think you want wantedSubset to conform to a type where each property is either true or itself an object of that type. We need to do a bit of footwork to get TypeScript to infer such a type where the value true is treated as type true and not boolean (at least until TS3.4 comes out and gives us const contexts):
type WantedSubset = { [k: string]: true | WantedSubset };
const asWantedSubset = <RP extends WantedSubset>(wantedSubset: RP) => wantedSubset;
const wantedSubset = asWantedSubset({
user: {
name: true,
profile: {
isCool: true
}
}
}); // no error, so that type works.
Now comes the type RecursivePick and friends. First of all, given a type T, you want a WantedSubset type that conforms to it. I'll call that a RecusivePicker<T>:
type RecursivePicker<T> = {
[K in keyof T]?: T[K] extends object | undefined ? RecursivePicker<T[K]> : true
}
So if T is Schema, then RecursivePicker<Schema> gives you:
type RPSchema = RecursivePicker<Schema>
// type RPSchema = {
// user?: {
// age?: true;
// name?: true;
// profile?: {
// isCool?: true;
// }
// }
// }
That can be a constraint on the type of wantedSubset that you'll allow to pick from Schema types.
And here is RecursivePick in all its recursive mapped conditional horror glory:
type RecursivePick<T, KO extends RecursivePicker<T>> =
Pick<{ [K in keyof T]: K extends keyof KO ?
KO[K] extends true ? T[K] :
KO[K] extends infer KOK ?
KOK extends RecursivePicker<T[K]> ? RecursivePick<T[K], KOK> :
never : never : never }, keyof T & keyof KO>;
It basically walks down through properties of T and examines the corresponding property of KO. If the property is true it returns the property of T unchanged. If the property is itself a bag of properties it recurses down. And if the property isn't there, it returns never. And that whole thing is Picked so that only keys appearing in both T and KO appear in the final output (this is a bit of fiddling to make sure that all the relevant mapped types are homomorphic, meaning that optional properties stay optional).
Let's verify it works:
type ExpectedType = RecursivePick<Schema, typeof wantedSubset>;
You can walk through that, but let's make the compiler verify it:
type ExpectedTypeManual = {
user: {
name: string;
profile: {
isCool?: boolean;
};
};
};
type MutuallyExtends<T extends U, U extends V, V=T> = true
// if no error in the next line, then ExpectedType and ExpectedTypeManual are
// structurally the same
type NoErrorHere = MutuallyExtends<ExpectedType, ExpectedTypeManual>
So that all works. Your function would be typed something like this:
declare function gql<Q extends RecursivePicker<S>, S>(
query: Q | true,
schema: S
): RecursivePick<S, Q>;
const result = gql(wantedSubset, null! as Schema); // looks good
Getting the implementation of the function to compile with no error might require an overload as conditional types are notoriously difficult to infer:
function gql<Q extends RecursivePicker<S>, S>(
query: Q | true,
schema: S
): RecursivePick<S, Q>;
function gql(query: any, schema: object): object {
return {}; // something better here
}
All right, hope that helps; good luck!
ExpectedType['user']['profile']['isCool']to betrue | undefinedand notboolean | undefined?boolean | undefined- fixed