helios1138 / graphql-typed-client

A tool that generates a strongly typed client library for any GraphQL endpoint. The client allows writing GraphQL queries as plain JS objects (with type safety, awesome code completion experience, custom scalar type mapping, type guards and more)
MIT License
212 stars 18 forks source link

Execution result type includes the full schema #12

Open Pajn opened 5 years ago

Pajn commented 5 years ago

It would be very nice if the result type only included the selected fields. I imagine this could be solvable by using a combination of mapped types and conditional types.

Have you looked into this at and have some experience on the possibility?

mikecann commented 5 years ago

Agreed, this is very almost perfect, except for that point... ill have a quick play and see if I can crack it..

helios1138 commented 5 years ago

@Pajn I can maybe see how mapped types can be used to pick the first level properties based on the query, but I can't see a solution on how to make it work with an arbitrarily nested GraphQL query/response.

Possibly there is a way to have at least some N-levels-down solution...

helios1138 commented 5 years ago

Okay, looked at conditional types. This shows more promise

mikecann commented 5 years ago

It can definitely be done. I had to head out but will take a look at it tomorrow or the day after if you dont work it out before hand :)

helios1138 commented 5 years ago

@mikecann this is what I have so far. Appears to be working recursively.

interface User {
  id: string
  name: string
  email: string | null
  posts: Post[]
}

interface Post {
  id: string
  title: string
  description: string | null
  author: User
}

interface UserRequest {
  id?: boolean | number
  name?: boolean | number
  email?: boolean | number
  posts?: PostRequest
}

interface PostRequest {
  id?: boolean | number
  title?: boolean | number
  description?: boolean | number
  author?: UserRequest
}

type UserPartial<R extends UserRequest> =
  (R extends { id: boolean | number } ? { id: string } : {}) &
  (R extends { name: boolean | number } ? { name: string } : {}) &
  (R extends { email: boolean | number } ? { email: string | null } : {}) &
  (R extends { posts: PostRequest } ? { posts: PostPartial<R['posts']>[] } : {})

type PostPartial<R extends PostRequest> =
  (R extends { id: boolean | number } ? { id: string } : {}) &
  (R extends { title: boolean | number } ? { title: string } : {}) &
  (R extends { description: boolean | number } ? { description: string | null } : {}) &
  (R extends { author: UserRequest } ? { author: UserPartial<R['author']> } : {})

const query = <T>(r: T extends UserRequest ? UserRequest : T): UserPartial<T> => {
  throw new Error('')
}

mikecann commented 5 years ago

Oh lol, you beat me too it, this is what I just came up with:

image

// The main grapQL type
type List = {
  name: string;
  age: number;
  something: {
    foo: "bar"
  }
};

type Req<T> = { [P in keyof T]?: number | boolean } & {
  __typename?: boolean | number;
  __scalar?: boolean | number;
};

function makeExecute<U>() {
  return function execute<T extends Req<U> >(
    request: T & Req<U>,
    defaultValue?: U
  ): { [P in keyof T]: P extends keyof U ? U[P] : never } {
    throw "put implementation here";
  }
}

const execute = makeExecute<List>();
const x = execute({ name: 1, something: 1  });
mikecann commented 5 years ago

Ill see if I can make mine recursive with your types..

mikecann commented 5 years ago

image

interface User {
  id: string
  name: string
  email: string | null
  posts: Post[]
}

interface Post {
  id: string
  title: string
  description: string | null
  author: User
}

type RequestProp<T> = T extends string ?  number | boolean :  Req<T>;

type Req<T> = { [P in keyof T]?: RequestProp<T[P]> } & {
  __typename?: boolean | number;
  __scalar?: boolean | number;
};

function makeExecute<U>() {
  return function execute<T extends Req<U> >(
    request: T & Req<U>,
    defaultValue?: U
  ): { [P in keyof T]: P extends keyof U ? U[P] : never } {
    throw "put implementation here";
  }
}

const x = makeExecute<Post>()({ title: 1,  author: { id: 1 } });

const y = makeExecute<User>()({  });

gotta go to bed now, but its almost there, just needs to handle arrays as part of that conditional.

mikecann commented 5 years ago

Oh man I must have been tired, because the above doesnt work for recursive types..

mikecann commented 5 years ago

image

Here this solves it recursively, it makes your brain bleed a little trying to work it out tho..

type Scalar = string | number | boolean;

interface User {
  id: string;
  name: string;
  age: number;
  email: string | null;
  posts: Post[];
}

interface Post {
  id: string;
  title: string;
  description: string | null;
  author: User;
}

type Selectable = number | boolean;

type RequestSelctionField<TFieldValue> = TFieldValue extends Scalar
  ? Selectable
  : TFieldValue extends (infer X)[]
  ? RequestSelection<X>
  : RequestSelection<TFieldValue>;

type RequestSelection<TObject> = {
  [P in keyof TObject]?: RequestSelctionField<TObject[P]>
} & {
  __typename?: Selectable;
  __scalar?: Selectable;
};

type ReturnProp<
  TObject,
  TRequestSelection,
  TField extends keyof TObject,
  TObjectValue = TObject[TField]
> = TObjectValue extends Scalar
  ? TObjectValue
  : TField extends keyof TRequestSelection
  ? TObjectValue extends (infer X)[]
    ? Returned<X, TRequestSelection[TField]>[]
    : Returned<TObject[TField], TRequestSelection[TField]>
  : never;

type Returned<TObject, TRequestSelection> = {
  [TField in keyof TRequestSelection]: TField extends keyof TObject
    ? ReturnProp<TObject, TRequestSelection, TField>
    : never
};

function makeExecute<TObject>() {
  return function execute<TRequest extends RequestSelection<TObject>>(
    request: TRequest & RequestSelection<TObject>,
    defaultValue?: TObject
  ): Returned<TObject, TRequest> {
    throw "put implementation here";
  };
}

const x = makeExecute<Post>()({
  title: 1,
  author: { id: 1, age: 1, posts: { id: 1 } }
});

const xx = x.author.posts[0].
mikecann commented 5 years ago

BTW using the above one could make a very nice fluent syntax using just a bit more typing and proxy objects.

image

type QueryArgsMap = {
  markTagSuggestions: QueryMarkTagSuggestionsArgs;
  marks: QueryMarksArgs;
  mark: QueryMarkArgs;
};

type QueryArgs<P, T> = P extends keyof QueryArgsMap ? QueryArgsMap[P] : never;

type Executable<TObject> = {
  execute: <TRequest extends RequestSelection<TObject>>(
    request: TRequest & RequestSelection<TObject>
  ) => Returned<TObject, TRequest>;
};

type MakeQueryable<T> = {
  [P in keyof T]: (variables: QueryArgs<P, T>) => Executable<T[P]>
};

type Root = {
  query: MakeQueryable<Query>;
};

let root: Root = {} as any;

const resp = root.query.marks({ input: { query: "foo" } }).execute({ totalCount: 1, marks: { id: 1 } });
resp.marks[0].

The above is using the schema from one of my own projects but will work with any object type.

... thinking about it more one could just export GraphQL AST much like https://github.com/apollographql/graphql-tag does, then it could be consumed by Apollo Client and you wouldnt have to write your own client with caching and all other other features that Apollo supports..

maybe https://github.com/prisma/nexus already does most of this..

helios1138 commented 5 years ago

Here this solves it recursively, it makes your brain bleed a little trying to work it out tho..

do you think your solution can fit the __scalar logic and return types that are unions/interfaces, not specific objects? the solution I'm currently considering relies on pre-generating the conditional types like these

export type UserPartial<
  R extends UserRequest,
  F1 extends UserRequest = _FR<R['friends'], UserRequest>,
  F2 extends PostRequest = _FR<R['posts'], PostRequest>,
  F3 extends PetRequest = _FR<R['pets'], PetRequest>
> = (R extends Required<Pick<R, 'id'>> ? { id: ID } : {}) &
  (R extends Required<Pick<R, 'username'>> ? { username: String } : {}) &
  (R extends Required<Pick<R, 'email'>> ? { email: String } : {}) &
  (R extends Required<Pick<R, 'wasEmployed'>> ? { wasEmployed: Boolean | null } : {}) &
  (R extends Required<Pick<R, 'friends'>> ? { friends: UserPartial<F1>[] | null } : {}) &
  (R extends Required<Pick<R, 'posts'>> ? { posts: PostPartial<F2>[] } : {}) &
  (R extends Required<Pick<R, 'pets'>> ? { pets: PetPartial<F3>[] } : {})

It also does not yet implements __scalar or unions/interfaces, but at least I can see how it can potentially be implemented, since all the information is there during the generation process. not sure how can that information can be extracted in your solution, if it only relies on inference. Maybe only pre-generate the lists of scalar props. Curious.

BTW using the above one could make a very nice fluent syntax using just a bit more typing and proxy objects.

not exactly sure what you have there. is the goal to get the same API as it is now, but with less pre-generated ts code?

you wouldnt have to write your own client with caching and all other other features that Apollo supports..

Currently, you can also use Apollo client here easily, inside the fetching mechanism

mikecann commented 5 years ago

@helios1138 Awesome. I think you should go with your technique if you can get it to work.

I spent half a day trying to come up with a clever non / minimal codegen solution when I should have been working on my project and although theoretically I could make it work I just dont have the time for it right now.

BTW I even spent a coupple of hours on a pair programming session with a TS evangelist who works closely with the compiler team on this problem :) Its a really fun problem to solve, wish I had a bit more time on it.

For now tho, go with your solution if it works. Im keen to use it in my project!

jgoux commented 4 years ago

Hello,

I'm very interested by this client and I think this part is the last missing piece. 😍 I very much agree with @mikecann on this

... thinking about it more one could just export GraphQL AST much like https://github.com/apollographql/graphql-tag does, then it could be consumed by Apollo Client and you wouldnt have to write your own client with caching and all other other features that Apollo supports..

I'd like a fully typed equivalent of graphql-tag to pass to any client (in my case I'd use react-query). It's like in CSS in JS, they all went from the css taged template to a typed object alternative, and I feel that we're very close to it here.

How can I help?

remorses commented 4 years ago

Graphql Zeus uses a generic type MapType to select some fields DST from an interface SRC

type MapType<SRC extends Anify<DST>, DST> = ...

View the code here

We can simply copy paste that code and have the feature right now