Skip to content
This repository has been archived by the owner on Dec 8, 2021. It is now read-only.

Generated interfaces for input types should use undefined instead of null #278

Closed
cyrus-za opened this issue Nov 10, 2018 · 21 comments
Closed

Comments

@cyrus-za
Copy link

cyrus-za commented Nov 10, 2018

Description

When a field is optional in the schema, graphqlgen generates the types as

    firstName: string | null;
    lastName: string | null;
  }

It is expected to rather generate using undefined instead of null or the ? shorthand.

  export interface UserInput {
    firstName?: string;
    lastName?: string;
  }

This issue only occurs when tsconfig.json has strict: true which comes with graphql-boilerplate.

Steps to reproduce

Clone this repo and do a tsc build to see the errors.
https://github.com/cyrus-za/graphqlgen-example

Expected results

No error.

Actual results

src/resolvers/Query.ts:3:14 - error TS2322: Type '{ user: (parent: {}, args: ArgsUser, ctx: Context) => User | undefined; }' is not assignable to type 'Type'.
  Types of property 'user' are incompatible.
    Type '(parent: {}, args: ArgsUser, ctx: Context) => User | undefined' is not assignable to type '(parent: {}, args: ArgsUser, ctx: Context, info: GraphQLResolveInfo) => User | Promise<User | nul...'.
      Type 'User | undefined' is not assignable to type 'User | Promise<User | null> | null'.
        Type 'undefined' is not assignable to type 'User | Promise<User | null> | null'.

 export const Query: QueryResolvers.Type = {

Versions

  • graphqlgen: 0.3.0
  • OS name and version: MacOS 10.14.1

PS. This is related to #50 but I was asked to create a new issue. @schickling

@schickling schickling changed the title Generated interfaces use null instead of undefined. Generated interfaces for input types should use undefined instead of null Nov 10, 2018
@kevinmarrec
Copy link

kevinmarrec commented Nov 10, 2018

@cyrus-za Everything looks fine for me :
screenshot_2

It was not working before, and had been fixed days ago in one of last releases, as said @schickling on #50 :

I assume this has been fixed by the latest version. Please open a new issue if this problem still occurs.

This is your custom https://github.com/cyrus-za/graphqlgen-example/blob/master/src/types.ts which is not valid and cause the error.

You might ask why using ? is not valid ? In fact that's somehow difficult to understand.
The TypeScript language indeed use ? for optional values, BUT GraphQL servers can't return undefined values.

GraphQL servers automatically map undefined values as null in client responses.

On the other hand, using strict: true enables the strictNullChecks option of TypeScript, which make you aware, for example, if one of your function return undefined instead of null (without the option, TypeScript doesn't care, and GraphQL will gently handle it by mapping undefined to null for responses).

And this is what happening in your code here : https://github.com/cyrus-za/graphqlgen-example/blob/master/src/resolvers/Query.ts#L5

The typing mismatching is between :

So you need to replace undefined by null in your types.ts to have it working, but then you probably can have new typing mistmatchings against your User definition.

@Weakky
Copy link
Contributor

Weakky commented Nov 10, 2018

@kevinmarrec We still have an issue with input types though.

An optional GraphQL input field can either be undefined (in the sense of typescript), null, or have a value.

Given the following schema:

input UserInput {
  field: String
}

type Query {
  users(input: UserInput): [User!]!
}

The following queries are all valid:

{
  users { ... }
}
{
  users(input: null) { ... }
}
{
  users(input: { ... }) { ... }
}

We need to change the typings for input types and make optional GraphQL fields for input types defined as so: field?: type | null

@schickling What do you think?

@kevinmarrec
Copy link

kevinmarrec commented Nov 10, 2018

@Weakky Alright, I didn't go deep testing GraphqlGen with inputs types yet.

Anyway, if I'm right, there is already an opened issue about the input types.

The error in Actual Results section of this issue isn't related on input types, it's about his custom service returning undefined. But if he fixes it, he then will have the error related to the input types issue, I guess.

@cyrus-za
Copy link
Author

cyrus-za commented Nov 12, 2018

@kevinmarrec that is exactly the problem. It is one or the other. I am also not aware of the graphql spec saying GraphQL servers automatically map undefined values as null in client responses.
If you can show me where it says that, I'd love to see it.

@kevinmarrec
Copy link

kevinmarrec commented Nov 14, 2018

@cyrus-za I don't know where it says that, but maybe there is an explanation : graphql/graphql-js#731

@cyrus-za
Copy link
Author

@kevinmarrec that is graphql-js which we are not using. We using a schema file directly. Nowhere in the facebook graphql spec does it say that undefined means null. Neither will it as some languages dont even have a null type.

This is an opinionated approach one way or another. I am just of the opinion that if a model interface has optional question mark (which as per typescript documentation means T | undefined) then the resolver typings should also use T | undefined or even better: just add the question mark similar to the actual interface of the model.

@kevinmarrec
Copy link

Alright then I don't know.

All I know is that if your make your resolver return undefined, it's resolved to null in client response 😁

@cyrus-za
Copy link
Author

cyrus-za commented Nov 14, 2018 via email

@joshuatshaffer
Copy link

I think there is a misunderstanding. @kevinmarrec is talking about "value types", things the server returns to the client, but @Weakky (and I think @cyrus-za as well) are talking about "input types", things the client sends to the server. As far as I can tell, no one actually disagrees with each other, you're just talking about different things.

My 2 cents:
The GraphQL spec only says that undefined values returned to the client will be replaced with null.

If result is null (or another internal value similar to null such as undefined or NaN), return null.

The spec does not say anything about undefined inputs sent to the server being replaced with null. The most it says is

In addition to not accepting the value null, it [a non-nullable input] also does not accept omission.

Which implies that there may be a difference between passing null and leaving an input undefined.

Given that some JavaScript implementations of GraphQL will pass omitted inputs as undefined, the generated types for inputs should cover this extra case. argName?: argType | null

@cyrus-za
Copy link
Author

cyrus-za commented Nov 15, 2018 via email

@vdiaz1130
Copy link

vdiaz1130 commented Nov 17, 2018

I think this issue shows up when using Prisma with graphqlgen.

Prisma generates the following interface:

export interface UserUpdateInput {
  email?: String;
  name?: String;
  password?: String;
  emailVerified?: Boolean;
}

GraphQlGen does this:

export interface UserUpdateInput {
  email: string | null
  name: string | null
  password: string | null
  emailVerified: boolean | null
}

Here's the mutation in schema.graphql

# import UserUpdateInput from './generated/prisma-client/prisma.graphql'
type Mutation {
    updateUser(data: UserUpdateInput!): User!
}

type User {
    email: String!
    id: ID!
    name: String!
}

User in datamodel.prisma

type User {
    id: ID! @unique
    email: String! @unique
    name: String!
    password: String!
    emailVerified: Boolean
    createdAt: DateTime!
    updatedAt: DateTime!
}

After generating the code with prisma generate, followed by graphql get-schema then graphqlgen, I get the error when implementing the resolver for createuser mutation:

Type 'import(".../src/generated/graphqlgen").MutationResolvers.UserUpdateInput' is not assignable to 
Type 'import(".../src/generated/prisma-client/index").UserUpdateInput'.
Types of property 'name' are incompatible.
Type 'string | null' is not assignable to type 'string | undefined'.
Type 'null' is not assignable to type 'string | undefined'. [2322]

Is there a workaround for this (or am I doing something wrong)? Been stuck on this for 2 days =\

@Weakky
Copy link
Contributor

Weakky commented Nov 18, 2018

Hey there,

@joshuatshaffer got it right! Here is the full problem:

GraphQL does indeed accept null as input type when a field is nullable, and from the perspective of graphqlgen, we should indeed type the input fields as field?: type | null

The problem is that prisma-client does not accept null for its input type. My guess is that it didn't make sense to accept such a value when querying your Prisma GQL API using prisma-client when it was designed.

This means that even if we fix this issue on graphqlgen right now and type all the nullable input field as field?: type | null, there will still be a type mismatch because undefined | null !== undefined

In order to fix this, we need to both fix prisma-client and graphqlgen to type all nullable input fields as field?: type | null

@schickling @timsuchanek: What do you think?

@nikolasburk
Copy link
Contributor

I just found out that the same problem applies to the Flow client as well! I've added a note to the corresponding issue in the prisma repo: prisma/prisma#3621 (comment)

@kinolaev
Copy link
Contributor

kinolaev commented Dec 3, 2018

Hello!
I made PRs for graphqlgen and graphql-binding based on how resolvers and binding actually work in this example:

const { graphql } = require("graphql")
const { makeExecutableSchema } = require("graphql-tools")
const { Binding } = require("graphql-binding")

const schema = makeExecutableSchema({
  typeDefs: `
    type Query {
      echo(input: [String]): [String]
      binding(input: String): [String]
    }
  `,
  resolvers: {
    Query: {
      echo: (root, { input }) => {
        const output = Array.isArray(input)
          ? input.map(e => (e === null ? undefined : e))
          : input
        console.log("resolver input", input)
        console.log("resolver output", output)
        return output
      },
      binding(root, { input }, { binding }, info) {
        console.log("binding input", input)
        return binding.query.echo({ input }, info).then(output => {
          console.log("binding output", output)
          return output
        })
      }
    }
  }
})

const root = undefined
const context = {
  binding: new Binding({ schema })
}
const queries = [
  "{ echo }",
  "{ echo(input: null) }",
  '{ echo(input: "1") }',
  '{ echo(input: ["1", null]) }',
  "{ binding }",
  "{ binding(input: null) }",
  '{ binding(input: "1") }'
]

queries.reduce(
  (promise, query) =>
    promise.then(_ => {
      console.log("query", query)
      return graphql(schema, query, root, context).then(result => {
        if (result.errors) {
          console.error("Err", result.errors.map(e => e.message))
        } else {
          console.log("Ok", result.data)
        }
        console.log("---")
      })
    }),
  Promise.resolve()
)

Output:

query { echo }
resolver input undefined
resolver output undefined
Ok { echo: null }
---
query { echo(input: null) }
resolver input null
resolver output null
Ok { echo: null }
---
query { echo(input: "1") }
resolver input [ '1' ]
resolver output [ '1' ]
Ok { echo: [ '1' ] }
---
query { echo(input: ["1", null]) }
resolver input [ '1', null ]
resolver output [ '1', undefined ]
Ok { echo: [ '1', null ] }
---
query { binding }
binding input undefined
resolver input null
resolver output null
binding output null
Ok { binding: null }
---
query { binding(input: null) }
binding input null
resolver input null
resolver output null
binding output null
Ok { binding: null }
---
query { binding(input: "1") }
binding input 1
resolver input [ '1' ]
resolver output [ '1' ]
binding output [ '1' ]
Ok { binding: [ '1' ] }
---

Types:

interface BindingArgs {
  input: string | null | undefined
}
interface ResolverArgs {
  input: Array<string | null> | null | undefined
}
type ResolverReturns = Array<string | null | undefined> | null | undefined
type BindingReturns = Array<string | null> | null

For ResolverArgs graphqlgen generates now:

interface ResolverArgs {
  input: Array<string | null | undefined> | null | undefined
}

I have not found a way to pass undefined element to array, it always replaced with null but
and I can't remove | undefined because I don't know is this argument or return type in function printFieldLikeType (isInput is not true for scalars in arguments).

@b33son
Copy link

b33son commented Dec 7, 2018

Any update on what to do? For now I'm switching my types to field?: string | null but I don't know if this the the correct solution.

@nickluger
Copy link

There is a semantic difference IMHO between setting null vs undefined as input type of a field:

Using null: "Please set this field to null in the database.

Using undefined: "Ignore this field; do not touch the column/field in the DB."

An input field in a mutation, that is optional (In other words: that can be ommited), should be undefined/ommitable.

@joshuatshaffer
Copy link

@nickluger There is definitely a difference between null and undefined/omitted. I looked at the source code of the reference implementation, graphql-js. It makes meaningful distinctions between null and omitted arguments. For example, omitting arguments causes a default value to be used if a default is defined. This means that clients are supposed to allow the omission of arguments. However, passing an explicit undefined is impossible because GraphQL uses JSON and JSON cannot serialize things like { x: undefined }. So, leaving an argument undefined should be allowed, but defining an argument to be undefined is impossible.

@Weakky Unfortunately, Flow seems unable to make a type that allows both {} and { x: "some value" } but does not allow { x: undefined }. It seems the best type to use is { arg?: argType }. This would allow both {} and { arg: null } to comply with the standard. This would also allow { arg: undefined } which is not ideal, but I think the client implementations will just have to deal with it. They can just remove all arguments with values of undefined when serializing the queries.

@b33son For now I recommend using args: { arg?: argType } on your resolvers and args: { arg: argType | null } on the client side. This will ensure your resolvers can handle both omitted and null arguments while also preventing your clients from passing values of undefined which might not be handled well.

@b33son
Copy link

b33son commented Dec 12, 2018

I've got a script that imports graghqlgen.ts to my client project before tests and using the Mutation ResolverArgs for type safety on an automated way.

export interface ArgsSignup {
    email: string;
    password: string;
    username: string;
    fullName: string | null;
    phone: string | null;
    profileImgUrl: string | null;
    facebookHandle: string | null;
    instagramHandle: string | null;
    twitterHandle: string | null;
    snapchatHandle: string | null;
  }
const variables: MutationResolvers.ArgsSignup = {
      email: `test-${randNumber}@fakedom8in.com`,
      password: `${randNumber}`,
      username: `test-${randNumber}`,
      fullName: `test-${randNumber}`,
      phone: "",
      profileImgUrl: `test-${randNumber}`,
      facebookHandle: `test-${randNumber}`,
      instagramHandle: `test-${randNumber}`,
      twitterHandle: `test-${randNumber}`,
      snapchatHandle: `test-${randNumber}`,
    }

This will work great once the interface defines optional fields correctly.

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Dec 12, 2018

I have posted a comment with code samples on the prisma end of this thread here prisma/prisma#3621 (comment).

The TL;DR for TypeScript the most correct approach I see is almost what @Weakky stated but with the addition of supporting x: null | Foo where appropriate:

  1. nullable prisma client input args should be typed x?: null | Foo
  2. graphqlgen, for nullable graphql input args with null default, should produce x: null | Foo
  3. graphqlgen, for nullable graphql input args without defaults, should produce x?: null | Foo
GQL                  TS
a: String            a?: null | string
                      ^  ^
                      |  | 2. user may pass explicit null
                      | 1. user may pass nothing
                      
a: String = null     a: null | string
                        ^
                        | 1. user may pass explicit null
                        | 2. user may pass nothing, thus gql null default

My PR #329 should take care of point 2 coincidentally.

@jasonkuhrt
Copy link
Member

Hey everyone, worked a bit on this tonight, moving the existing PR forward. Almost there, except #331 (comment). I think I'll have it done this week, or next.

@jasonkuhrt
Copy link
Member

jasonkuhrt commented Jan 5, 2019

The PR also addressed resolver return types, not just resolver params. I'm not sure that was mentioned in this thread so just mentioning it.

I chose not to allow undefined in resolver return types but rather only explicit null. This improves DX by preventing developers from accidentally not returning. Its also smart enough to account for Arrays. Thanks to @kinolaev who originally penning the PR!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests