graphql-code-generator: Fragments do not combine arrays correctly

Describe the bug Including multiple fragments that contain the same arrays results in the types getting combined at the array level. TS doesn’t know how to merge these correctly. That is Array<TypeA> & Array<TypeB> is not equivalent to Array<TypeA & TypeB> because TS doesn’t merge arrays.

To Reproduce Steps to reproduce the behavior: This repro has the bug fully reproducible.

https://github.com/chrbala/apollo-bug-repro/tree/codegen-merge-arrays

  1. My GraphQL schema:
type Item {
  string: String!
  number: Int!
}

type Query {
  items: [Item!]!
}
  1. My GraphQL operations:
query Page {
  ...String
  ...Number
}

fragment String on Query {
  items {
    string
  }
}

fragment Number on Query {
  items {
    number
  }
}
  1. My codegen.yml config file:
overwrite: true
schema: "src/schema.js"
documents: "src/**/*.tsx"
generates:
  src/generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"

Expected behavior You should be able to access both the string and number properties on items, but you can only access string. If you switch the order of ...String and ...Number, then you can only access number.

The generated fragment is

export type PageQuery = (
  { __typename?: 'Query' }
  & NumberFragment
  & StringFragment
);

But for it to work correctly in TS, it would need to be more like

export type PageQuery = (
  { __typename?: 'Query' }
  & {
    items: Array<NumberFragment['items'][0] & StringFragment['items'][0]>
  }
);

Environment:

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20 (3 by maintainers)

Most upvoted comments

Hey @chrbala, I created https://github.com/dotansimha/graphql-code-generator/pull/6184 which changes the default behavior to generate more friendly types for all use-cases.

You can install the changes of the PR locally via

yarn add @graphql-codegen/typescript-operations@1.18.3-alpha-33caf3be.0 @graphql-codegen/typescript-react-apollo@2.2.8-alpha-33caf3be.0 @graphql-codegen/typescript@1.22.4-alpha-33caf3be.0

(as new commits are pushed the list of packages is updated over here

Please let me know whether this addresses your issues!

@dotansimha This is a bit tricky, as we generate types per fragment, but maybe there is some kind of merge TypeScript utility that can take two deep types and merge them?

export type PageQuery = (
  { __typename?: 'Query' } 
  & DeepMerge<NumberFragment, StringFragment>
)

It seems like this would be possible via recursive conditional types: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-rc/#recursive-conditional-types

@chrbala If you could create a PR with a failing test case, that would be highly appreciated! Edit: I just realized that it does not make sense to create failing test case without knowing the solution to the problem 😅

I can have a in-depth look at it this week!

Hi @dotansimha and @n1ru4l , I’m also running into errors related to this in @graphql-codegen/typescript versions 2.1.2. It seems that if you request an array of interfaces field multiple times, spreading fragments on them leads to invalid state. This could commonly happen when a set of subcomponents define fragments on the same type and each request different fields on an interface array. I have a reproducible repo here: https://codesandbox.io/s/gql-test-forked-j275t?file=/src/index.tsx (schema is found in src/schema.js, query is found in src/index.tsx)

You can pop a terminal and run yarn codegen to see the error.

relevant packages

    "@graphql-codegen/cli": "2.1.1",
    "@graphql-codegen/fragment-matcher": "3.1.0",
    "@graphql-codegen/introspection": "2.1.0",
    "@graphql-codegen/typescript": "2.1.2",
    "@graphql-codegen/typescript-operations": "2.1.2",
    "@graphql-codegen/typescript-react-apollo": "3.1.2",

here’s the schema and query:

Schema

interface IItem {
  sharedField: String
}

type ItemA implements IItem {
  sharedField: String
  itemAOnlyField: String
}

type ItemB implements IItem {
  sharedField: String
  itemBOnlyField: String 
}

type Parent { 
  parentField: String
  children: [IItem!]!
}

type Query {
  parent: Parent!
}

Repro Query

query Page {
    parent {
      parentField
      children {
        ... on ItemA {
          itemAOnlyField
        }
      }
      children {
        ... on ItemA {
          itemAOnlyField
        }
      }
    }
  }

My real use case looks something closer to the below gql operations, but after playing around I determined that it also happens if you request the interface array multiple times even with the same fragment type (seen above as children { ... on ItemA { itemAOnlyField } })

Real Query

  # defined in FirstComponent.tsx
  fragment FirstComponent_parent on Parent {
    parentField
    children {
      sharedField
      ... on ItemA {
        itemAOnlyField
      }
    }
  }

  # defined in SecondComponent.tsx
  fragment SecondComponent_parent on Parent {
    parentField
    children {
      sharedField
      ... on ItemB {
        itemBOnlyField
      }
    }
  }

  # defined in ParentComponent.tsx
  query ParentComponentQuery {
    parent {
      parentField
      ...FirstComponent_parent
      ...SecondComponent_parent
    }
  }

The error I get when trying to compile:

Found 1 error

  ✖ src/generated/graphql.tsx
    TypeError: Invalid state.
        at mergeSelectionSets (/sandbox/node_modules/@graphql-codegen/visitor-plugin-com
mon/index.cjs.js:447:19)
        at SelectionSetToObject.buildSelectionSetString (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2671:25)
        at /sandbox/node_modules/@graphql-codegen/visitor-plugin-common/index.cjs.js:262
1:41
        at Array.reduce (<anonymous>)
        at SelectionSetToObject._buildGroupedSelections (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2611:80)
        at SelectionSetToObject.transformSelectionSet (/sandbox/node_modules/@graphql-co
degen/visitor-plugin-common/index.cjs.js:2735:30)
        at SelectionSetToObject.buildSelectionSetString (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2685:89)
        at /sandbox/node_modules/@graphql-codegen/visitor-plugin-common/index.cjs.js:262
1:41
        at Array.reduce (<anonymous>)
        at SelectionSetToObject._buildGroupedSelections (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2611:80)
    TypeError: Invalid state.
        at mergeSelectionSets (/sandbox/node_modules/@graphql-codegen/visitor-plugin-com
mon/index.cjs.js:447:19)
        at SelectionSetToObject.buildSelectionSetString (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2671:25)
        at /sandbox/node_modules/@graphql-codegen/visitor-plugin-common/index.cjs.js:262
1:41
        at Array.reduce (<anonymous>)
        at SelectionSetToObject._buildGroupedSelections (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2611:80)
        at SelectionSetToObject.transformSelectionSet (/sandbox/node_modules/@graphql-co
degen/visitor-plugin-common/index.cjs.js:2735:30)
        at SelectionSetToObject.buildSelectionSetString (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2685:89)
        at /sandbox/node_modules/@graphql-codegen/visitor-plugin-common/index.cjs.js:262
1:41
        at Array.reduce (<anonymous>)
        at SelectionSetToObject._buildGroupedSelections (/sandbox/node_modules/@graphql-
codegen/visitor-plugin-common/index.cjs.js:2611:80)

Thanks for your help on this!