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 · 1 comment
Open

Codegen generates unused types from the schema #724

sebastijandumancic opened this issue May 23, 2024 · 1 comment

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.

The closest I've found to a workaround is for cases where the "selected" subtypes that are valid for the query all have some known common property, like id, then I can re-export it with a utility type like this to filter it :

// in this example, lots of subtypes include an `id` property typed as `string | null` so we make it the default 
type ExtractUsedTypes<Type, Key extends string = 'id', Matches = string | null> = Extract<NonNullable<TYpe>, { [key in Key]?: Matches }>

export type SomeType_fixed = Omit<SomeType, 'propertyWithTooManyTypes' | 'anotherOne'> & {
  // this strips out all the junk empty types that don't include `id: string | null`
  propertyWithTooManyTypes: ExtractUsedTypes<SomeType['propertyWithTooManyTypes']>
  // this strips out all the junk empty types that don't include `numericId: number`
  anotherOne: ExtractUsedTypes<SomeType['anotherOne'], 'numericId', number>
}

Then your actual code can import the fixed version of the type and you don't need to fill the code with junk guards for scenarios that you know can't happen in your setup.

Unfortunately, this adds a lot of brittle overhead to the type definitions, when half the point of using this library is to avoid hardcoding types and make the type follow changes to the schema and queries.

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