Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Codegen generates unused types from the schema #724

Open
sebastijandumancic opened this issue May 23, 2024 · 2 comments
Open

Codegen generates unused types from the schema #724

sebastijandumancic opened this issue May 23, 2024 · 2 comments

Comments

@sebastijandumancic
Copy link

sebastijandumancic commented May 23, 2024

Which packages are impacted by your issue?

No response

Describe the bug

I'm using codegen with typescript-operations and typescript-react-apollo to generate types for a graphql query. Here is the config:

import { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
 schema: {
   'http://localhost:8080/api': {
     headers: {
       Authorization: 'Bearer XXX',
     },
   },
 },
 documents: ['./**/*.graphql'],
 debug: true,
 ignoreNoDocuments: true,
 generates: {
   'modules/gql/': {
     plugins: ['typescript-operations', 'typescript-react-apollo'],
     preset: 'near-operation-file',
     presetConfig: {
       baseTypesPath: 'types.ts',
       importAllFragmentsFrom: 'types.ts',
     },
     config: {
       useTypeImports: true,
       avoidOptionals: true,
       extractAllFieldsToTypes: true,
       skipTypename: true,
       mergeFragmentTypes: true,
       withHooks: false,
       flattenGeneratedTypes: true,
       flattenGeneratedTypesIncludeFragments: true,
       dedupeFragments: true,
     },
   },
 },
};

export default config;

I have the following query in a file:

query FooterNavigation {
   entries(section: "footerNavigation", hasDescendants: true) {
       title
       descendants {
           __typename
           title
           ... on footerNavigation_footerNavigation_Entry {
               __typename
               id
               pageLink {
                   ... on pageLink_external_BlockType {
                       id
                       openInNewTab
                       title
                       externalUrl
                   }
                   ... on pageLink_internal_BlockType {
                       id
                       internalEntry {
                           title
                           url
                       }
                   }
                   ... on pageLink_product_BlockType {
                       id
                       productEntry {
                           title
                           url
                       }
                   }
                   ... on pageLink_commerce_BlockType {
                       id
                       productEntry {
                           title
                           url
                       }
                   }
               }
           }
       }
   }
}

The issue is that using the above config, I get following types for footernavigation_footerNavigation_Entry:
Screenshot 2024-05-23 at 16 01 28

Where FooterNavigationQuery_entries_about_about_Entry_descendants is an array of all possible types here from schema (which is huge), but in the query used for this component, I'm interested only in the last one from the screenshot: FooterNavigationQuery_entries_about_about_Entry_descendants_footerNavigation_footerNavigation_Entry

I don't understand why I'm getting unused types generated here, even if I've tried with related plugin options. In order to use this, I must type thin in a component by asking for __typename and again checking the type:

  if (data.__typename !== 'footerNavigation_footerNavigation_Entry') {
       return null;
   }

   return data.pageLink.map(item => {
       if (item?.__typename === 'pageLink_commerce_BlockType')
           return 'la';
       if (item?.__typename === 'pageLink_external_BlockType')
           return 'la'
       if (item?.__typename === 'pageLink_internal_BlockType')
           return 'la'
       if (item?.__typename === 'pageLink_product_BlockType')
           return 'la'
   })

Which seems unnecessary since the query itself is querying just the footer-related fields. If I remove __typename in a query, I'll still get multiple types, just without that field. By using typename I can at least target the correct type, but it seems like unnecessary work. Thanks!

Your Example Website or App

localhost

Steps to Reproduce the Bug or Issue

/

Expected behavior

I expect to get single type for the provided field in the query.

Screenshots or Videos

No response

Platform

  • OS: macOS
  • NodeJS: 20
  • graphql v16.8.1
  • @graphql-codegen/* 5.0.2

Codegen Config File

/* eslint-disable import/no-extraneous-dependencies */
import { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
schema: {
'http://localhost:8080/api': {
headers: {
Authorization: 'Bearer xxx',
},
},
},
documents: ['./**/*.graphql'],
debug: true,
ignoreNoDocuments: true,
generates: {
'modules/gql/': {
plugins: ['typescript-operations', 'typescript-react-apollo'],
preset: 'near-operation-file',
presetConfig: {
baseTypesPath: 'types.ts',
importAllFragmentsFrom: 'types.ts',
},
config: {
useTypeImports: true,
avoidOptionals: true,
extractAllFieldsToTypes: true,
skipTypename: true,
mergeFragmentTypes: true,
withHooks: false,
flattenGeneratedTypes: true,
flattenGeneratedTypesIncludeFragments: true,
dedupeFragments: true,
},
},
},
};

export default config;

Additional context

No response

@AlanSl
Copy link
Contributor

AlanSl commented Oct 16, 2024

I have the same issue, and I found this discussion: dotansimha/graphql-code-generator#5567

Unfortunately it looks like this is considered a feature not a bug 😢 because "technically" a hypothetical server could return an empty object derived from any non-selected subtype of the type named in the schema. We know our actual server and client logic wouldn't allow this in real-life data, but the logic that prevents it exists outside of the schema and query that the generator can see.

I feel like it's common enough for GraphQL servers and clients to be configured to only allow data linkages that match
the query selections, that there should be an option to ignore non-selected subtypes that return no data or only __typename from the generated union type (or at the very least, allow collapsing them all into one { __typename: string } or { __typename: 'AAA' | 'AAB' | 'AAC' ... } instead of dozens or even hundreds of { __typename: 'AAA' } | { __typename: 'AAB' } | { __typename: 'AAC' } | ...). In my case for example, there about 50-70 subtypes of a "content fragment" type that is used everywhere any content references other content, and in each of dozens or hundreds of such cases, it is configured to only allow a subset of 1-5 content types which are then the ones that are included in the query.

There aren't many good workarounds either. If TypeScript had anything like an Exact or NoAdditionalProperties feature to stop object types from being extended with additional properties, we could maybe do some type mapping like T extends NoAdditionalProperties<{ __typename: string }> ? never : T but sadly it doesn't despite a lot of demand for that feature, there's no way to I can see to differentiate object types with __typename and nothing else from ones with __typename and other unknown properties.

@AlanSl
Copy link
Contributor

AlanSl commented Jan 29, 2025

I found a good-enough workaround.

type IgnoreTypenameOnlyObjects<T> = 
  T extends { __typename?: Maybe<string>; } // If any key is __typename...
  ? keyof T extends "__typename" // ...and, EVERY key is __typename...
    ? never // ...discard this subtype.
    : T // Keep any object with __typename and any other key
  : T; // Keep everything else, e.g. non-objects or objects without __typename

export type DeepIgnoreTypenameOnlyObjects<Item> = {
  [K in keyof Item]: DeepIgnoreTypenameOnlyObjects<
    IgnoreTypenameOnlyObjects<Item[K]>
  >;
};

Then wrap your complex union type in DeepIgnoreTypenameOnlyObjects before exporting it, and all the junk like ... | { __typename?: 'UnusedType' } | { __typename?: 'AnotherUnusedType' } | etc with no actual properties should disappear.

I recommend also wrapping it in a type resolver utility (like https://stackoverflow.com/a/69288824/568458) so the type is readable in IDEs else it becomes very unwieldy and indirect.

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

No branches or pull requests

2 participants