dotansimha / graphql-code-generator

A tool for generating code based on a GraphQL schema and GraphQL operations (query/mutation/subscription), with flexible support for custom plugins.
https://the-guild.dev/graphql/codegen/
MIT License
10.86k stars 1.33k forks source link

Avoid wrapping everything in Maybe #3919

Closed honi closed 4 years ago

honi commented 4 years ago

Is your feature request related to a problem? Please describe.

This is my codegen config (some configs don't change from the defaults but I have them there since I've been playing around with different combinations):

{
    "schema": "./schema.graphql",
    "documents": "src/**/*.graphql",
    "generates": {
        "src/generated/graphql.tsx": {
            "plugins": [
                "typescript",
                "typescript-operations",
                "typescript-react-apollo"
            ],
            "config": {
                "reactApolloVersion": 2,
                "withComponent": false,
                "withHOC": false,
                "withHooks": true,
                "withMutationFn": true,
                "withRefetchFn": true,
                "avoidOptionals": false,
                "preResolveTypes": false
            }
        }
    }
}

This is my schema:

type AccountRegionType {
  id: String!
  name: String!
}

type Query {
  accountRegions: [AccountRegionType]
}

This is the generated code:

export type AccountRegionType = {
   __typename?: 'AccountRegionType';
  id: Scalars['String'];
  name: Scalars['String'];
};

export type Query = {
   __typename?: 'Query';
  accountRegions?: Maybe<Array<Maybe<AccountRegionType>>>;
};

This is how I'm using the generated React Apollo hook:

import {useGetAccountRegionsQuery} from 'generated/graphql';

const AccountRegions = () => {
    const {
        data: {accountRegions = []} = {},
        loading: accountRegionsLoading,
    } = useGetAccountRegionsQuery();

    if (accountRegionsLoading) return <div>Loading...</div>;

    return (
        <ul>
            {(accountRegions || []).map(region => (
                <li>{region?.name}</li>
            ))}
        </ul>
    );
};

export default AccountRegions;

All query results are wrapped with Maybe<>, which leads to a lot of unneeded type checking or casting in my components to avoid TypeScript compiler errors.

I didn't find any config option to generate the code the way I would like it to be. But maybe it's like this for a reason I'm not seeing.

My backend is implemented in Python/Django using Graphene. As far as I can tell, I can't see in which scenarios my data will actually be null to justify wrapping the results with Maybe<>.

Describe the solution you'd like

I would like to be able to write my component like this:

import {useGetAccountRegionsQuery} from 'generated/graphql';

const AccountRegions = () => {
    const {
        data: {accountRegions = []} = {},
        loading: accountRegionsLoading,
    } = useGetAccountRegionsQuery();

    if (accountRegionsLoading) return <div>Loading...</div>;

    return (
        <ul>
            {accountRegions.map(region => (
                <li>{region.name}</li>
            ))}
        </ul>
    );
};

export default AccountRegions;

I want accountRegions to be fully defined so that I don't have to add defaults or use the ? operator.

I believe this would be possible if the generated types look like this (no wrapping Maybe<>):

export type AccountRegionType = {
   __typename?: 'AccountRegionType';
  id: Scalars['String'];
  name: Scalars['String'];
};

export type Query = {
   __typename?: 'Query';
  accountRegions: Array<AccountRegionType>;
};

Is it possible with some specific config options to achieve this? Or am I just using the hook wrong?

ardatan commented 4 years ago

If you have nonnullable fields as you describes, you can fix this in your schema.

type Query {
  accountRegions: [AccountRegionType!]!
}`

Just make that field nonnullable array with nonnullable values and that's it. You don't have Maybe in your generated queries, because we cannot just remove Maybe. Removing Maybe would make generated typings incorrect.

honi commented 4 years ago

Awesome! Thank you for the quick response.

My schema is generated automatically by Graphene. So I'll look there how to define the queries as non nullable (related graphene django issue).

ardatan commented 4 years ago

So you can override Maybe generic. See here; https://graphql-code-generator.com/docs/generated-config/typescript#maybevalue-string-default-value-t--null

honi commented 4 years ago

That works!

My updated codegen config looks like this (removed some non related parts for clarity):

{
    "schema": "./schema.graphql",
    "documents": "src/**/*.graphql",
    "generates": {
        "src/generated/graphql.tsx": {
            "plugins": [
                "typescript",
                "typescript-operations",
                "typescript-react-apollo"
            ],
            "config": {
                "maybeValue": "T"
            }
        }
    }
}

Not sure if this will generate other problems, but for now everything is working fine :)

cncolder commented 3 years ago

You can add a custom hook.

    hooks:
      afterOneFileWrite:
        - node codegenCustomHook.js
        - prettier --write

Then you can do whatever you want.

const generatedFiles = process.argv.slice(2)
vbrzezina commented 2 years ago

Hello guys, I just got back to this issue after few years hoping to make it work this time, but I seem to be stuck at the same dead end, I'm not sure if I'm doing something wrong, or there's only a few people who care about having it typed strictly and correctly (but that's the point of this project right), so I decided to finally reach for help..

Basically, I have the same issue explained in this issue, but I don't think that the issue wasn't really resolved..

  1. ✅ I understand why general types are optional, meaning if I don't use the option onlyOperationTypes and it generates types for all types in my schema and not specific operations then it makes sense to have the fields optional, because you do not have to query them right? Example: I have this type type Global { createdAt: DateTime favicon: UploadFileEntityResponse footer: ComponentLayoutFooter locale: String localizations: GlobalRelationResponseCollection metaTitleSuffix: String! metadata: ComponentMetaMetadata navbar: ComponentLayoutNavbar notificationBanner: ComponentElementsNotificationBanner updatedAt: DateTime } type GlobalEntity { attributes: Global id: ID! } until I specify a query query GetGlobal($locale: I18NLocaleCode!) { global(locale: $locale) { data { id i cannot be sure what I will recieve - in case of above query i'd only get id
  2. ❌ But.. To the point. If I stay with the example above, the moment I specify a query clearly specifing i wanna query the field which is marked as required it should now not display the maybe right? I ran into some discussion on reddit where someone suggest it actually works like this, only problem is that it either doesnt or I'm missing some part of the config which I couldn't figure out from the docs and from my opinion it should be default
  3. ❌ It's kinda clear that overriding the Maybe to be just T creates the exact opossite issue
  4. ❌ How doest avoidOptionals help with anything? Sure, there is a difference between null and undefined, but.. the consequences connected to this issue are the same - you cannot read property of null and end up with ?.this?.beautiful?.codebase

I minimized my config to only using typescript and operations `overwrite: true schema: "http://localhost:1337/graphql" documents: "./graphql/*/.graphql" generates: api/operations.ts: plugins:

sabidhasan commented 2 years ago

the moment I specify a query clearly specifing i wanna query the field which is marked as required it should now not display the maybe right?

@starkys-brzezina The Maybe types come from the schema for your Global type, so if a field in there is nullable, it will be nullable regardless of whether you query it or not.

vbrzezina commented 2 years ago

the moment I specify a query clearly specifing i wanna query the field which is marked as required it should now not display the maybe right?

@starkys-brzezina The Maybe types come from the schema for your Global type, so if a field in there is nullable, it will be nullable regardless of whether you query it or not.

@sabidhasan that's not what I meant :) I'm saying it's marked as Maybe even if it's not nullable.. I already narrowed down the problems a little, but it will probably take me some more time to get it all together in some PR

vbrzezina commented 2 years ago

Okayyy, I guess I just figured where I've gone wrong.. I probably didn't realize, but the graphql endpoint I work with might have not exactly standard resolvers. In my case, it always return the query field with data (and errors) inside - meaning even if the entity is not found, i always get the query name in the operation result - like this

Screenshot 2022-09-27 at 15 50 44

After investigating I realized that this doesn't have to be standard at all and the query can return null right away, although now I'm wondering wether this pattern of always getting data and errors can be a matter of some configuration.. Shame I realized this after trying to rewrite part of the plugin for eight hours.. Either way I think it would be nice to have some option to generally set the root types as non nullable..

The other thing I was struggling with is that there's probably no standardized way how arrays are returned, meaning you can get field: null, field: [], or even field: [null] which is like super awesome

vbrzezina commented 2 years ago

For those of you struggling with this in Strapi as well, maybe this link will help https://github.com/strapi/strapi/issues/4548

nanafan93 commented 1 year ago

Okay here is what I don't quite understand. Given a schema like this:

schema {
  query: Query
}

type Query {
  myChats: [String!]
}

The typescript types get generated as shown below

export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

export type Query = {
  __typename?: 'Query';
  myChats?: Maybe<Array<Scalars['String']>>;
};

My question is why does the type have to "double-down" on the nullability. The typescript type is saying that the myChats property might be missing (?) and further if it exists its wrapped in a Maybe type again ? Why can't it be only one of them. The way I understand this schema is that myChats may be missing. But if it is present, it will be an array which may have 1 or more strings.

ctsstc commented 11 months ago

I've landed here trying to find out if the generated types can have a required field but with a "Maybe" so that you always have the field but potentially it could be a value or null. It doesn't seem that's easily available, and seems that the generation allows for the field to either be there, or not, or undefined, or null. I'm just wondering if this is on purpose for some reason, or if going from GQL to TS doesn't translate well so this is just the "safer" approach to cover everything? If GQL exposed a "Maybe/nullable/union types" then I think what I was after would be feasible (requiring the field but allowing a value or null)

iamcrisb commented 9 months ago

The solution I came up with is the following:

{
  config: {
    wrapEntireFieldDefinitions: true,
    entireFieldWrapperValue: 'T extends Array<infer U> ? Array<NonNullable<U>> : T'
  }
}

It basically removes the maybe value for array values. I feel this should be the default behaviour; this way you don't need to modify the graphql schema and still get the full benefits of typescript.

mattiLeBlanc commented 1 month ago

@dotansimha I find the Maybe value super annoying to be honest, it is hard to override in typescript. I love using the generated typings from the schema in my services, but the maybe is just making it so annoying. So I started adding ! to make some properties required, which I think is good practice, and I want to properties that are not required to have to optional ? flag in my typings. But the Maybe value is just so difficult to get rid off, I have to do a hard cast to another scalar type. It would be nice if there is a way to just turn of the Maybe wrapping all together.

mattiLeBlanc commented 1 day ago

@iamcrisb what about normal scalar values like a string? \ image

I get these TS issue in my code which forces me to either use maybe in my function input definition but I don't want to use that. I just use { tvName?: string }.

Is there a workaround to get rid of these annoying maybes?