Why does @graphql-codegen/typescript declare the resolver parent type to be an output type?

161 views Asked by At

I am using @graphql-codegen/typescript to generate types for this graphql schema:

type Book {
  title: String
  author: String
  comment: String
}

type Query {
  books: [Book]
}

Excerpt from the generated code:

export type Book = {
  __typename?: 'Book';
  author?: Maybe<Scalars['String']['output']>;
  comment?: Maybe<Scalars['String']['output']>;
  title?: Maybe<Scalars['String']['output']>;
};

The TS type "Book" is the type used for the parent that gets passed to the resolvers for the fields of GQL type "Book".

I DO understand that the fields of GQL type "Book" are not marked as non-null, so a query might get null as a response value for any of them, and I also understand that any query may choose to omit a field from the response.

However, these things are irrelevant here because the TS type "Book" is not what gets passed in the response, but what gets passed to the field resolvers as the input. "Book" looks like it is an output type for the query, but is used as a resolver input type.

Therefore, I do not understand why the fields in Book are optional and typed as Maybe. Omitted fields don't have their resolver called, and erroneous fields result from returning null or throwing an error in the resolver, both of which don't even have a well-defined meaning for the input of the resolvers.

To further understand what gets passed around, I have implemented the resolvers for Book as follows:

const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
  },
  {
    title: 'City of Glass',
    author: 'Paul Auster',
  },
];

const resolvers: Resolvers = {
  Query: {
    books: () => books,
  },
  Book: {
    title: (book: Book) => {
      console.log("title", book);
      return book.title!;
    },
    comment: (book: Book) => {
      console.log("comment", book);
      return "foobar";
    }
  },
};

So I am returning an object that has title and author, but no comment, from the parent resolver, then generate a comment on-the-fly in the "comment" resolver. I also log the parent object. The code gets run on Apollo Server.

Observation: The parent object that gets passed to the field resolvers is what I returned from the parent resolver, i.e. without a comment field and with both the title and author fields present, even when the actualy query does not ask for those fields.

It seems sensible to me that this "internal" object from the parent resolver gets passed to the field resolvers; it is just how I understand resolvers to work.

What is totally confusing, though, is that the code generator generates the type Book in the style of an "output" type, with all fields optional/Maybe and generated fields present, for that resolver input.

What am I missing?

Edit: Here are some more of the generated type definitions:

export type Maybe<T> = T | null;
export type Scalars = {
  ID: { input: string; output: string; }
  String: { input: string; output: string; }
  Boolean: { input: boolean; output: boolean; }
  Int: { input: number; output: number; }
  Float: { input: number; output: number; }
};
1

There are 1 answers

3
Michel Floyd On

In GraphQL terminology an input type is used when defining inputs to a mutation. The GraphQL type that is defined in your schema and the GraphQL type of the parent of a resolver are the same. TypeScript is overlaying it's own terminology on all this so it can be confusing.

In this case codegen has defined identical input and output keys for its Scalars type which only adds to the confusion.

A field resolver (i.e. the resolver that resolves a field on a type) is unaware of the content of your upstream data beyond what the GraphQL type tells it.

Consider what would happen if you changed you books constant to:

const books = [
  {
    title: 'The Awakening',
    author: 'Kate Chopin',
  },
  {
    title: 'City of Glass',
    comment: 'Break in case of emergency',
  },
];

This would be fine given all your Book fields are nullable. The typeScript type of the parent to the resolvers would also still be valid.

A resolver takes up to 4 arguments:

resolverFunction: (parent,args,context,info) => {…code…}

The function signature is the same whether you are resolving a Query or the field for a type. In the former case parent will be undefined. In the latter case parent will be the parent object and it will be of the type of the parent. In practice the parent object may be incomplete (which makes sense since if you are resolving a field that hasn't been resolved yet then the parent will not have a value for that field). The parent object may also have additional fields that are not defined in its type. This is so extra data can easily be passed down to the field resolvers.