Which packages are impacted by your issue?
@graphql-codegen/visitor-plugin-common
@graphql-codegen/typescript-operations
Describe the bug
@graphql-codegen/typescript-operations v6 throws
TypeError: Unexpected type. (from
@graphql-codegen/visitor-plugin-common@7.x selection-set-to-object.ts,
buildSelectionSet) whenever a fragment is spread with a conditional
directive (@include / @skip / @defer) and that fragment's
top-level selection set contains an InlineFragment or FragmentSpread.
The GraphQL document is fully valid (it parses, validates against the schema,
and was handled correctly by typescript-operations v5). v6 aborts codegen
for the entire document.
Root cause
In buildFragmentSpreadsUsage, only FIELD nodes inside the spread
fragment's top-level selection set are wrapped with fragmentDirectives;
INLINE_FRAGMENT and FRAGMENT_SPREAD nodes pass through as raw AST nodes:
const fragmentSelectionNodes = [
...fragmentSpreadObject.node.selectionSet.selections,
].map(originalNode => {
if (originalNode.kind === Kind.FIELD) {
return { ...originalNode, fragmentDirectives: [...spread.directives] };
}
return originalNode; // INLINE_FRAGMENT / FRAGMENT_SPREAD pass through raw
});
For a conditional spread, _buildGroupedSelections then inlines those
raw nodes into flattenedSelectionNodes and feeds the result to
buildSelectionSet, which only accepts FIELD and DIRECTIVE for nodes
that have a kind property — any other kind hits
throw new TypeError('Unexpected type.').
The non-conditional spread path doesn't hit this because it routes
FragmentSpreadUsage.selectionNodes through flattenSelectionSet
recursively (line ~545), which correctly expands inline fragments and
nested spreads.
Your Example Website or App
https://github.com/vkbansal-rubrik/codegen-conditional-spread-bug
Steps to Reproduce the Bug or Issue
git clone https://github.com/vkbansal-rubrik/codegen-conditional-spread-bug.git
cd codegen-conditional-spread-bug
pnpm install (or npm install / yarn install)
pnpm generate (or npm run generate)
- Observe:
❯ Generate
✖ Generate [FAILED: Unexpected type.]
TypeError: Unexpected type.
at SelectionSetToObject.buildSelectionSet
.../visitor-plugin-common/esm/selection-set-to-object.js:541:27
at collectGrouped
.../visitor-plugin-common/esm/selection-set-to-object.js:327:80
at .../visitor-plugin-common/esm/selection-set-to-object.js:372:25
at Array.reduce (<anonymous>)
at SelectionSetToObject._buildGroupedSelections
.../visitor-plugin-common/esm/selection-set-to-object.js:319:43
The shipped document.graphql is Case 1 below. The repo's README.md
contains drop-in replacements for the other three cases — paste any one
into document.graphql and re-run pnpm generate to verify the matrix.
Case matrix
| Variant of the spread fragment's top-level selections |
@include on spread? |
v6 result |
Inline fragments (... on X { ... }) (shipped Case 1) |
yes |
FAILS "Unexpected type." |
Named fragment spreads (...Other) (Case 2) |
yes |
FAILS "Unexpected type." |
Only FIELD nodes (Case 3) |
yes |
OK |
| Inline fragments (Case 4) |
no (unconditional spread) |
OK |
Expected behavior
As a user, I expected the document above to produce a valid
TypeScript type for LibraryQuery (matching v5 behavior, where the
Publication union variants are optional when $includeFeatured is false),
but instead codegen throws TypeError: Unexpected type. and aborts the
entire output.
With the proposed fix applied, the generated type is:
export type LibraryQuery = {
library: {
id: string;
name: string;
featured:
| { id: string; title: string }
| { id: string; issue: number }
| Record<PropertyKey, never>
| null;
} | null;
};
— i.e., the union variants appear when $includeFeatured = true, and the
| Record<PropertyKey, never> member covers $includeFeatured = false.
Screenshots or Videos
N/A — repro is a one-file Node script; full output is in Steps to Reproduce.
Platform
- OS: macOS (Darwin 25.5.0)
- NodeJS: 24.13.0 (also reproduced on 20.x)
graphql version: 15.8.0 (also reproduces on 16.x)
@graphql-codegen/* versions:
@graphql-codegen/typescript-operations: 6.0.2
@graphql-codegen/visitor-plugin-common: 7.0.1 and 7.0.2
@graphql-codegen/core: 6.0.0
Codegen Config File
schema: schema.graphql
documents: document.graphql
generates:
types.ts:
plugins:
- typescript-operations
(No special config; bug reproduces with default options.)
Additional context
Status of contributor workflow
Workaround (until merged)
Restructure the fragment so its top-level selection set contains only
FIELD nodes — i.e., split type-narrowing inline fragments into helper
fragments per concrete type and spread each helper directly at the
consumer.
Before:
fragment FooFragment on HierarchyObject {
id
... on Cdm { pendingX { ...XFragment } }
... on Polaris { pendingY { ...YFragment } }
}
# consumer
...FooFragment @include(if: $enabled)
After:
fragment FooBaseFragment on HierarchyObject { id }
fragment FooCdmFragment on Cdm { pendingX { ...XFragment } }
fragment FooPolarisFragment on Polaris { pendingY { ...YFragment } }
# consumer
...FooBaseFragment @include(if: $enabled)
...FooCdmFragment @include(if: $enabled)
...FooPolarisFragment @include(if: $enabled)
Proposed fix (in the PR)
In _buildGroupedSelections, after a conditional FragmentSpreadUsage is
inlined into flattenedSelectionNodes, partition the result and route
top-level INLINE_FRAGMENT / FRAGMENT_SPREAD nodes through the existing
schema-aware flattenSelectionSet (which already expands them per type)
before handing the FIELD-only merged set to buildSelectionSet.
This preserves buildSelectionSet's contract that raw AST nodes are
FIELD or DIRECTIVE. Existing FIELD-only callers are byte-for-byte
unaffected.
Verification
- Without fix: the two new tests fail with the same
TypeError: Unexpected type. originating from selection-set-to-object.ts:805.
- With fix:
- 20/20 tests in
ts-documents.skip-include-directives.spec.ts pass.
- 236/237 tests in the
typescript-operations suite pass (the 1
failing snapshot, imports external custom scalar in shared type file when said scalar is used in relevant Input, also fails on
master without the patch — it is unrelated to this change).
- 50/50 tests in the
visitor-plugin-common suite pass.
Which packages are impacted by your issue?
@graphql-codegen/visitor-plugin-common@graphql-codegen/typescript-operationsDescribe the bug
@graphql-codegen/typescript-operationsv6 throwsTypeError: Unexpected type.(from@graphql-codegen/visitor-plugin-common@7.xselection-set-to-object.ts,buildSelectionSet) whenever a fragment is spread with a conditionaldirective (
@include/@skip/@defer) and that fragment'stop-level selection set contains an
InlineFragmentorFragmentSpread.The GraphQL document is fully valid (it parses, validates against the schema,
and was handled correctly by
typescript-operationsv5). v6 aborts codegenfor the entire document.
Root cause
In
buildFragmentSpreadsUsage, onlyFIELDnodes inside the spreadfragment's top-level selection set are wrapped with
fragmentDirectives;INLINE_FRAGMENTandFRAGMENT_SPREADnodes pass through as raw AST nodes:For a conditional spread,
_buildGroupedSelectionsthen inlines thoseraw nodes into
flattenedSelectionNodesand feeds the result tobuildSelectionSet, which only acceptsFIELDandDIRECTIVEfor nodesthat have a
kindproperty — any other kind hitsthrow new TypeError('Unexpected type.').The non-conditional spread path doesn't hit this because it routes
FragmentSpreadUsage.selectionNodesthroughflattenSelectionSetrecursively (line ~545), which correctly expands inline fragments and
nested spreads.
Your Example Website or App
https://github.com/vkbansal-rubrik/codegen-conditional-spread-bug
Steps to Reproduce the Bug or Issue
git clone https://github.com/vkbansal-rubrik/codegen-conditional-spread-bug.gitcd codegen-conditional-spread-bugpnpm install(ornpm install/yarn install)pnpm generate(ornpm run generate)The shipped
document.graphqlis Case 1 below. The repo'sREADME.mdcontains drop-in replacements for the other three cases — paste any one
into
document.graphqland re-runpnpm generateto verify the matrix.Case matrix
@includeon spread?... on X { ... }) (shipped Case 1)...Other) (Case 2)FIELDnodes (Case 3)Expected behavior
As a user, I expected the document above to produce a valid
TypeScripttype forLibraryQuery(matching v5 behavior, where thePublicationunion variants are optional when$includeFeaturedis false),but instead codegen throws
TypeError: Unexpected type.and aborts theentire output.
With the proposed fix applied, the generated type is:
— i.e., the union variants appear when
$includeFeatured = true, and the| Record<PropertyKey, never>member covers$includeFeatured = false.Screenshots or Videos
N/A — repro is a one-file Node script; full output is in Steps to Reproduce.
Platform
graphqlversion: 15.8.0 (also reproduces on 16.x)@graphql-codegen/*versions:@graphql-codegen/typescript-operations: 6.0.2@graphql-codegen/visitor-plugin-common: 7.0.1 and 7.0.2@graphql-codegen/core: 6.0.0Codegen Config File
(No special config; bug reproduces with default options.)
Additional context
Status of contributor workflow
packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts).packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts).Workaround (until merged)
Restructure the fragment so its top-level selection set contains only
FIELD nodes — i.e., split type-narrowing inline fragments into helper
fragments per concrete type and spread each helper directly at the
consumer.
Before:
After:
Proposed fix (in the PR)
In
_buildGroupedSelections, after a conditionalFragmentSpreadUsageisinlined into
flattenedSelectionNodes, partition the result and routetop-level
INLINE_FRAGMENT/FRAGMENT_SPREADnodes through the existingschema-aware
flattenSelectionSet(which already expands them per type)before handing the FIELD-only merged set to
buildSelectionSet.This preserves
buildSelectionSet's contract that raw AST nodes areFIELD or DIRECTIVE. Existing FIELD-only callers are byte-for-byte
unaffected.
Verification
TypeError: Unexpected type.originating fromselection-set-to-object.ts:805.ts-documents.skip-include-directives.spec.tspass.typescript-operationssuite pass (the 1failing snapshot,
imports external custom scalar in shared type file when said scalar is used in relevant Input, also fails onmasterwithout the patch — it is unrelated to this change).visitor-plugin-commonsuite pass.