typescriptapolloprismaapollo-server

Forced to specify relation values in resolvers with ApolloServer and TypeScript


I am using Apollo Server with TypeScript types generation.

The GraphQL type I am working on at the moment is the following.

type User {
  id: ID!
  email: String!
  name: String!
  posts: [Post!]!
  reactions: [Reaction!]!
}

type Query {
  me: User
}

NOTE: the ! after the arrays is voluntary. I don't want those to be potentially null, I want them to be arrays that are worse case empty.

In my base query resolver, I implement me as following.

  me() {
    return {
      id: '123',
      name: 'Joe',
      email: 'joe@example.com',
    };
  },

I am leaving out posts and reactions, as they will be resolved down the chain by the user resolver object (which is implemented, returning [] in both cases for the moment).

Doing that does not make the type system happy, as the me() function in the query resolver is typed me?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType>;, which -- long story short -- has to return a User.

// Code generated by Apollo Server
export type User = {
  __typename?: 'User';
  email: Scalars['String']['output'];
  id: Scalars['ID']['output'];
  name: Scalars['String']['output'];
  posts: Array<Post>;
  reactions: Array<Reaction>;
};

Here, the posts and reactions arrays are required.

Back to the query resolvers, if I implement me() like following ...

  me() {
    return {
      id: '123',
      name: 'Joe',
      email: 'joe@example.com',
      posts: [],
      reactions: [],
    };
  },

..., then the type system is happy. Considering the types being generated, I understand why.

My first question is the following: are those generated types wrong? Since I am allowed to not specify some values of my return type to let the resolvers chain do its work, the generated types should be expecting something like Partial<User>.

Additional "what the...?" moment: returning undefined for those 2 properties ...

  me() {
    return {
      id: '123',
      name: 'Joe',
      email: 'joe@example.com',
      posts: undefined,
      reactions: undefined,
    };
  },

... actually works and makes TS happy, even though User states that those are mendatory and cannot be null or undefined.

Proving null also makes TS happy.

I am wondering of course first if I am not holding something wrong.

But it feels like the generated types should be allowing you to not provide all values, to let them be resolved further down the chain.

If I force-typecast as User, if also works and the resolvers chain calls the posts() of my userResolvers object (as it should).

This example may be trivial, but then when you return an array of User, and you get it from the database (eg: using Prisma), I have for now to .map all of them to insert the missing posts: undefined, reactions: undefined to make the type system happy, and that despite the examples I find of Apollo Server and Prisma being integrated together without those map.

I should be able to, in the end, just do

  async me(_parent, _args, { db }) {
    return await db.user.findFirstOrThrow({ where: { email: 'joe@example.com' } });
  },

without having to

  async me(_parent, _args, { db }) {
    const me = await db.user.findFirstOrThrow({ where: { email: 'joe@example.com' } });
    return { ...me, posts: undefined, reactions: undefined };
  },

I am using the latest versions of all NPM packages (as of November 19th, 2023).


Solution

  • TL;DR: The answer is here.

    It is possible to specify a default mapper in the configuration to Partial<{T}>.

    // codegen.ts
    import type { CodegenConfig } from '@graphql-codegen/cli';
    
    const config: CodegenConfig = {
      overwrite: true,
      schema: './src/typeDefs.graphql',
      generates: {
        'src/__generated__/graphql.ts': {
          plugins: ['typescript', 'typescript-resolvers'],
        },
      },
      config: {
        useIndexSignature: true,
        contextType: '../context#Context',
        defaultMapper: 'Partial<{T}>', // 💡 This line here!
      },
    };
    
    export default config;
    

    That will cause the TypeScript generated types to go from

    User: ResolverTypeWrapper<User>;
    

    to

    User: ResolverTypeWrapper<Partial<User>>;
    

    Then, your resolvers are allowed to return a User without providing its posts or reactions attributes and let the resolver chain do its magic (calling the proper UsersResolver if the query asks for that information).