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).
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).