Graphql-code-generator: Strange TypeScript behavior with resolvers unless wrapped in NonNullable

Created on 18 Oct 2019  路  9Comments  路  Source: dotansimha/graphql-code-generator

I was trying to figure out why I kept getting this error despite following the advice of the docs and issues to set useIndexSignature: true:

error TS2322: Type '{ Query: { bananas: ResolverFn<ResolverTypeWrapper<string>, {}, any, {}> | undefined; }; Mutation: {}; }' is not assignable to type 'IResolvers<any, any> | IResolvers<any, any>[] | undefined'.
  Type '{ Query: { bananas: ResolverFn<ResolverTypeWrapper<string>, {}, any, {}> | undefined; }; Mutation: {}; }' is not assignable to type 'IResolvers<any, any>'.
    Property 'Query' is incompatible with index signature.
      Type '{ bananas: ResolverFn<ResolverTypeWrapper<string>, {}, any, {}> | undefined; }' is not assignable to type '(() => any) | GraphQLScalarType | IEnumResolver | IResolverObject<any, any, any> | IResolverOptions<any, any, any>'.
        Type '{ bananas: ResolverFn<ResolverTypeWrapper<string>, {}, any, {}> | undefined; }' is not assignable to type 'IResolverObject<any, any, any>'.
          Property 'bananas' is incompatible with index signature.
            Type 'ResolverFn<ResolverTypeWrapper<string>, {}, any, {}> | undefined' is not assignable to type 'IResolverObject<any, any, any> | IResolverOptions<any, any, any> | IFieldResolver<any, any, any>'.
              Type 'undefined' is not assignable to type 'IResolverObject<any, any, any> | IResolverOptions<any, any, any> | IFieldResolver<any, any, any>'.

5 const apolloServer = new ApolloServer({typeDefs, resolvers});
                                                   ~~~~~~~~~

  node_modules/apollo-server-core/dist/types.d.ts:48:5
    48     resolvers?: IResolvers | Array<IResolvers>;
           ~~~~~~~~~
    The expected type comes from property 'resolvers' which is declared here on type 'Config'

I realized that when I had my resolvers in a different file like:

// resolvers.ts
import bananas from "./resolver/bananas";
import {Resolvers} from "./types-generated";

let resolvers: Resolvers;
export default resolvers = {
    Query: {
        bananas,
    },
};

```typescript
// resolvers/bananas.ts
import {QueryResolvers} from "../types-generated";
const bananas: QueryResolvers["bananas"] = async () => "Hello, Banana!";
export default bananas;

two unexpected behaviors would happen:
1. The ApolloServer constructor would complain about the resolver possibly being undefined.
2. Type checking on the resolver's arguments and return value does not work.

When I wrapped my resolver's type in NonNullable like this:
```typescript
// resolvers/bananas.ts
import {QueryResolvers} from "../types-generated";
const bananas: NonNullable<QueryResolvers["bananas"]> = async () => "Hello, Banana!";
export default bananas;

the above two issues are fixed. Also interesting is that ApolloServer doesn't complain even if I have useIndexSignature: false instead of true.

I'm not sure a fix for this would be in the form of documentation or code. The documentation route would be the easiest. If the fix was done via code, maybe you could generate a type for each resolver function, so instead of QueryResolver["bananas"] you would use BananasResolver, which would already be wrapped with NonNullable. I'm not familiar with the internals so perhaps there is a better approach than what I suggested.

  1. My GraphQL schema:
type Query {
    bananas: String!
}
  1. My codegen.yml config file:
overwrite: true
schema: src/graphql/type-defs.ts
generates:
    src/graphql/types-generated.d.ts:
        plugins:
            - typescript
            - typescript-resolvers
        config:
            noSchemaStitching: true
            useIndexSignature: true
require:
    - ts-node/register/transpile-only

Environment:

  • OS: MacOS Catalina
  • @graphql-codegen/...: 1.8.1
  • NodeJS: v12.12.0
  • TypeScript: 3.6.4
  • apollo-server-core: 2.9.6
waiting-for-answer

Most helpful comment

The solution for the author's problem might be:

export const resolvers: QueryResolvers = {};

resolvers.a = async (_, args, context) => {
}

resolvers.b = async (_, args, context) => {
}

Such way, it is possible to distribute resolvers across different files, but having arguments be strictly implicitly typed

All 9 comments

@msheakoski
Can you please try to use Resolvers type, and not the child types? I think the index signature is being applied only for the root type and not for the child resolvers types.

@dotansimha Yes, you are correct that the main Resolvers type behaves as designed when working from a single file containing all resolvers.

What if I have, for example, 100 different resolvers? I would not want to inline all of that code into a single file because it would be thousands of lines long and hard to maintain.

Given this use case, what would be the best way to type my resolvers when they are in separate files?

I think adding a note to the docs or maybe even a generated comment above the Resolvers type to wrap the resolver in NonNullable would be helpful to anybody in a similar situation.

The other solution that comes to mind would be adding something like a useIndividualResolverTypes option that generates types like BananaQueryResolver, LogInMutationResolver, GetCommentsQueryResolver. However, I'm not sure if that is worth all of the extra work when simply wrapping the resolver type in NonNullable achieves the same effect.

The main Resolvers just points to sub-types of resolvers, you can use QueryResolvers (or any TypeResolvers) as well and it will work the same, this way you can write it in different files (and those type doesn't enforce you to implement all fields in the same file, it just provides validation for field names and types)

I am currently using it as you suggested, with QueryResolvers and MutationResolvers. The problem is that QueryResolvers is generated like this:

export type QueryResolvers<
  ContextType = any,
  ParentType extends ResolversParentTypes["Query"] = ResolversParentTypes["Query"]
> = {
  bananas?: Resolver<
    ResolversTypes["String"],
    ParentType,
    ContextType
  >
}

Take special note of the ?: because this causes ApolloServer to complain only when using QueryResolvers in a different file. It does not happen when all of the resolvers are inlined together in a single file. It does not matter if useIndexSignature is set to true or false as suggested in #1133:

Error:(9, 3) TS2322: Type '{ Query: { bananas: ResolverFn<...> | undefined; }; }' is not assignable to type 'IResolvers<any, any> | IResolvers<any, any>[] | undefined'.
  Type '{ Query: { bananas: ResolverFn<...> | undefined; }; }' is not assignable to type 'IResolvers<any, any>'.
    Property 'Query' is incompatible with index signature.
      Type '{ bananas: ResolverFn<...> | undefined; }' is not assignable to type 'GraphQLScalarType | (() => any) | IEnumResolver | IResolverObject<any, any, any> | IResolverOptions<any, any, any>'.
        Type '{ bananas: ResolverFn<...> | undefined; }' is not assignable to type 'IResolverObject<any, any, any>'.
          Property 'bananas' is incompatible with index signature.
            Type 'ResolverFn<ResolverTypeWrapper<string>, {}, any, {}> | undefined' is not assignable to type 'IResolverObject<any, any, any> | IResolverOptions<any, any, any> | IFieldResolver<any, any, any>'.
              Type 'undefined' is not assignable to type 'IResolverObject<any, any, any> | IResolverOptions<any, any, any> | IFieldResolver<any, any, any>'.

What I am mainly trying to say is that any developer using the very common workflow of having resolvers in separate files along with the most popular GraphQL package, Apollo, will get an unexpected TypeScript error unless they wrap their resolvers with NonNullable<>.

This is a wonderful tool, and I am just trying to help improve the developer experience even more so that others do not have to go down the same journey of confusion, and trial and error that I went through. I could put together a small PR for the typescript-resolvers docs if you would like, otherwise, at least now there is something searchable on GitHub that others experiencing the same problem can reference.

I see, and if you do:

const queryResolvers: QueryResolvers = { ... }
const resolvers: Resolvers = { queryResolvers };

// Then use `resolvers` in Apollo Server

Does it work? @msheakoski

@dotansimha I tried to reproduce my issue in a CodeSandbox example, but strangely I do not experience it there.

I am going to make a minimal example locally to see if I can pinpoint what might be causing it. So far, all of the projects that I have used graphql-code-generator with have been based on Next.js, so I want to rule out any side effects from using libraries or frameworks. I will report back with my results.

Thanks @msheakoski , waiting for your update :)

Closing.
@msheakoski feel free to update and we can open this again if it's still relevant.

The solution for the author's problem might be:

export const resolvers: QueryResolvers = {};

resolvers.a = async (_, args, context) => {
}

resolvers.b = async (_, args, context) => {
}

Such way, it is possible to distribute resolvers across different files, but having arguments be strictly implicitly typed

Was this page helpful?
0 / 5 - 0 ratings