Graphql-code-generator: Parent type for resolver coming from a query resolver

Created on 9 Nov 2018  路  5Comments  路  Source: dotansimha/graphql-code-generator

To handle typings of a query resolver that doesn鈥檛 directly return the right JSON, but passes data to other resolvers down the line (as parent data) I needed some weird stuff.

The use case is like this one:
https://github.com/howtographql/howtographql/issues/619#issuecomment-385309639

Problem 1: One cannot use the query resolver type as-is, since it expects a particular return type
Problem 2: One cannot use the parent/root type from the other resolver type as it is whatever the one in Problem 1 returned

I ended up using only the argument types of the query resolver and returning something arbitrary. Then I extracted the type of that return value and used that as the type of the parent argument of the other resolver. So now the return value of the query resolver is the (parent) argument type of another resolver.

// resolvers/Query.ts
export default {
  feed: async (...allArgs: ArgumentTypes<QueryResolvers.FeedResolver>) => {
     const [, { filter, skip, first, orderBy }, context] = allArgs
     ...
     return {a:1, b:2}
  }
}
// resolvers/Feed.ts
import { FeedResolvers, MyContext } from '../generated/resolvers-types'
import queryResolver from './Query'
export default {
  links: async (parent, _args, context, info) => {
    // typeof parent  is now the right one: Promise<{a: number, b: number}>
    const p = await parent
    return (await parent).links
  },
  found: async (parent, _args) => {
    return (await parent).links.length
  },
} as FeedResolvers.Resolvers<MyContext, ReturnType<typeof queryResolver.feed>>

and i defined this two helper types

export type ArgumentTypes<F> = F extends (...args: infer A) => infer R ? A : never
// The default ReturnType generic breaks because of parent being of type never in the query resolver
export type ReturnType<F> = F extends (...args: infer A) => infer R ? R : never

Convoluted but it is super useful to know exactly what the parameter is, instantly found a bug.
Would it be possible to have helpers for that in the generated code somehow?

btw, I'm new to Typescript so be easy on me please

Most helpful comment

Thanks for the very comprehensive answer!

My point is that the Query resolver won't return what is expected (in your example returns a string instead), so the typings for that can't be used directly. Furthermore I wanted to have the return value of that query be automatically _in synch_ with the parent argument of the User resolver.

That is why I extracted the ArgumentsType from QueryResolvers.FeedResolver but let the return type to be inferred, and then extracted that inferred return type and used it as the parent type of the Feed resolver.

// resolvers/Query.ts
export default {
  feed: async (...allArgs: ArgumentTypes<QueryResolvers.FeedResolver>) => {
     const [, { filter, skip, first, orderBy }, context] = allArgs
     ....
// resolvers/Feed.ts
import { FeedResolvers, MyContext } from '../generated/resolvers-types'
import queryResolver from './Query'
export default {
  links: async (parent, _args, context, info) => {
    // typeof parent  is now the right one: Promise<{a: number, b: number}>
  ...
} as FeedResolvers.Resolvers<MyContext, ReturnType<typeof queryResolver.feed>>

(ReturnType is not the "native" one because that one assumes there are no args of type never)

Since that was useful for me, I thought you may have a comment on that approach and if it makes sense to support it directly from the generated code somehow.

All 5 comments

@kamilkisiela :)

I have no clue what is the issue here.

Since resolver has defined type for its parent and it doesn't match what you have, it's possible to overwrite it.

For example:

type Query {
  post: Post
}

type Post {
  id: String
  author: User
}

type User {
  id: String
  name: String
}
  • type Query has no parent
  • type Post has Post as a parent
  • type User has User as a parent

type Query

  • posts and other fields in Query has never as a parent
  • posts expects Post to be returned

type Post

  • author and other fields in Post has Post as a parent
  • author expects User to be returned

type User

  • name and other fields in User has User as a parent
  • name expects string to be returned

That's by default, but if you want to overwrite the parent you can use a generic type that each resolver related type has.

For example, if you pass a string to resolve a User (that string is an id or something) you can set string as a parent like this:

const User: UserResolvers.Resolvers<any, string> {
  id: (id, args, ctx, info) => id, // this is a string
  name: (id, args, ctx, info) => getUserById(id).name
};

The <any, string> sets any as context and string as parent.

There are two tests that might interest you:

Default parents
https://github.com/dotansimha/graphql-code-generator/blob/cdbaa3ca4b46cbdd946a1d38ea426981c95e5a5b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts#L343-L401

Custom parents thanks to Mappers
https://github.com/dotansimha/graphql-code-generator/blob/cdbaa3ca4b46cbdd946a1d38ea426981c95e5a5b/packages/templates/typescript-resolvers/tests/resolvers.spec.ts#L465-L524

Thanks for the very comprehensive answer!

My point is that the Query resolver won't return what is expected (in your example returns a string instead), so the typings for that can't be used directly. Furthermore I wanted to have the return value of that query be automatically _in synch_ with the parent argument of the User resolver.

That is why I extracted the ArgumentsType from QueryResolvers.FeedResolver but let the return type to be inferred, and then extracted that inferred return type and used it as the parent type of the Feed resolver.

// resolvers/Query.ts
export default {
  feed: async (...allArgs: ArgumentTypes<QueryResolvers.FeedResolver>) => {
     const [, { filter, skip, first, orderBy }, context] = allArgs
     ....
// resolvers/Feed.ts
import { FeedResolvers, MyContext } from '../generated/resolvers-types'
import queryResolver from './Query'
export default {
  links: async (parent, _args, context, info) => {
    // typeof parent  is now the right one: Promise<{a: number, b: number}>
  ...
} as FeedResolvers.Resolvers<MyContext, ReturnType<typeof queryResolver.feed>>

(ReturnType is not the "native" one because that one assumes there are no args of type never)

Since that was useful for me, I thought you may have a comment on that approach and if it makes sense to support it directly from the generated code somehow.

@dbuezas
By typings cannot be used directly, you mean that you need to overwrite the generics?

You don't have to force or use as with the resolvers typings, just use the as-is for the entire resolver object.
By default, the generated output of the resolvers plugin is an object matching the structure of your type. You can override it with TypeScript generics, and you are doing it only once, and not for each resolver field, because they should all be the same.

For example:

If you have a a type:

type MyType {
   id: ID!
   foo: String
   bar: Int
}

type Query {
   myType: MyType
}

Then, you resolvers with the generated typings should look like that:

import { MyTypeResolvers, QueryResolvers } from './generates.ts';

const myTypeResolvers: MyTypeResolvers.Resolvers = {
    id: (obj, args, context) => obj.id,
    foo: (obj, args, context) => obj.foo,
    bar: (obj, args, context) => obj.bar,
};

const myTypeResolver: QueryResolvers.MyTypeResolver = (root, args, context) => {
    return {id: '1', foo: '2', bar: 3};
}

export default {
    MyType: myTypeResolvers,
    Query: { myType: myTypeResolver },
};

Now, let's say that your actual objects looks a bit different, and you are changing your myType resolver to return it:

const myTypeResolver: QueryResolvers.MyTypeResolver = (root, args, context) => {
    return { _id: '1', inner: { foo: '1', bar: 2 }};
}

So declare a TypeScript interface that matches your object:

interface MyTypeCustomStructure {
   _id: string;
   inner: { foo: string; bar: number }; 
}

Next, use it in your Query resolver generics to tell the resolver how your Return object looks like:

const myTypeResolver: QueryResolvers.MyTypeResolver<MyTypeCustomStructure> = (root, args, context) => {
    return { _id: '1', inner: { foo: '1', bar: 2 }};
}

And also, use it in the type resolvers to tell it how it looks:

const myTypeResolvers: MyTypeResolvers.Resolvers<{}, MyTypeCustomStructure> = {
    id: (obj, args, context) => obj._id,
    foo: (obj, args, context) => obj.inner.foo,
    bar: (obj, args, context) => obj.inner.bar,
};

If you wish to avoid specifying the generics each time, you can use the mappers configuration, and this way you can just tell the plugin to always match between MyType and MyTypeCustomStructure:

schema: schema.graphql
generates:
   out.ts:
      config:
           mappers:
                MyType: MyTypeCustomStructure
     plugins:
          - add:
               - "import { MyTypeCustomStructure } from './my-types.ts';"
          - typescript-common
          - typescript-server
          - typescript-resolvers

@dotansimha
I have same problem. As you described above, CustomStructure can be ParentType. However, how can I use multiple ParentType? How can I achieve type information through [Type]Resolvers.Resolvers interface ?

My use-case

// for example, MyTypeDBModel is mongodb document.
type MyTypeParentType = MyTypeDBModel | MyType

const myTypeResolvers: MyTypeResolvers.Resolvers<{}, MyTypeParentType> = {
  id: (parent) => {
    return is<MyTypeDBModel>(parent) ? parent._id : parent.id
  }
}

How can I use MyTypeParentType as other Type's response type?

import { MyTypeParentType } from './path-to-my-type'

type OtherParentType = OtherModel | Other

const other: OtherResolvers.Resolver<{}, OtherParentType> = {
  myType: (source) => source.myType
}

  • generated myType signature = MyTypeResolver<MyType, TypeParent, Context>
  • My intent is MyTypeResolver<MyTypeParentType, TypeParent, Context>

    • ResponseType is matched based on declared information in myType.resolver

Should I do like this?

// interface field override pseudo code. so complicate 馃 
interface OtherResolver extends OtherResolvers.Resolver<{}, OtherParentType> {
  myType?: OtherResolvers.MyTypeResolver<MyTypeParentType, OtherParentType, {}>
}

const other: OtherResolver = {
  myType: (source) => source.myType
}
Was this page helpful?
0 / 5 - 0 ratings