问题
context
I am attempting to build a codegen tool. I enjoy using GraphQL, however, when I own the full stack, it seems a bit silly that my front-end calls my back-end with stringly defined gql queries. GQL is strongly typed, thus I should be able to provide strong typed queries and responses.
problem
I don't know how to build an interface such that I can recursively reflect and map an argument from an input type to a target type. Specifically, I want to map my query request type to a gql query response type.
const query: QueryRequest<Food.IFood> = {
name: true // true ==> implies inclusion in response
}
const res = await client.food(query)
console.log(res.name) // PASS - should compile
console.log(res.nodeId) // FAIL - should not compile. `nodeId` was not present in query
// Food.IFood is a TS interface, representative of my GQL schema. GQL => TS interfaces is a solved codegen problem already
// ref: https://github.com/cdaringe/gql-ts-client-codegen/blob/master/src/__tests__/fixture/namespace.ts
QueryRequest<T>
maps myFood.IFood
interface (incompletely) into a new type, where keys map to bools, indicating GQL field inclusion- However, each client method would need to sniff the passed
QueryRequest<T>
for explicit shape, and somehow map that explicit shape on to, essentially, aPartial<Food.IFood>
.- Cleary I don't want a
Partial
--aPartial
is ambiguous as to which fields are present. I want the client's response to have explicit field membership, as a function of the input.
- Cleary I don't want a
I understand that the above description of my GQL client is largely over-simplified and hand waiving other complexities required to be compliant with all GQL features. That's fine and good. My main objective in this post is strictly to see if there's a way to do this reflected type mapping.
I have begun sketching out a hard-coded target client.ts
file for what I'd like a potential output to look like here: https://github.com/cdaringe/gql-ts-client-codegen/blob/master/src/target.ts
Any input would be appreciated! Thanks.
回答1:
Unfortunately what you want is basically to constrain the type of a variable while at the same time get the compiler to infer the type for that variable. That in not unfortunately possible directly.
The only way to achieve the desired behavior is to use a function. Functions can have generic type parameters that have constraints placed on them but the final type parameter will be inferred from the actual object literal passed in:
type QueryRequest<T, K extends keyof T> = {
keys: Record<K, boolean>
}
function buildQueryRequest<T>() {
return function <P extends keyof T> (o:Partial<Record<P, boolean>>) : QueryRequest<T, P>{
return null!;
}
}
interface IFood {
name: string;
nodeId: number;
}
type QueryResult<T, K extends keyof T> = Pick<T, K>
declare class Client {
food<K extends keyof IFood>(q: QueryRequest<IFood, K>) : Promise<QueryResult<IFood, K>>
}
(async function (client: Client) {
const query = buildQueryRequest<IFood>()({
name: true // true ==> implies inclusion in response
})
const res = await client.food(query)
console.log(res.name) // PASS - should compile
console.log(res.nodeId) // error
})
buildQueryRequest
is a function that returns a function (ie a curried function) in order to allow for the first argument to be specified and the second one to be inferred,
来源:https://stackoverflow.com/questions/54055578/reflect-and-map-typed-argument-shape-in-typescript