From 8f44a85f6be8cab5fc3d593ee59f7806bb2831e6 Mon Sep 17 00:00:00 2001 From: Sylvain Utard Date: Wed, 15 Jul 2026 22:55:24 +0200 Subject: [PATCH 1/3] fix(visitor-plugin-common): keep deferred fields optional with overlapping named @defer spreads Expand @defer/@stream named fragment spreads into per-selection nodes so intersecting an outer field like `id` no longer collapses deferred-only fields to required. Co-authored-by: Cursor --- .changeset/defer-named-spread-optionality.md | 6 ++ .../src/selection-set-to-object.ts | 40 +++++++++-- ...-documents.skip-include-directives.spec.ts | 4 +- .../operations/tests/ts-documents.spec.ts | 67 ++++++++++++++++++- 4 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 .changeset/defer-named-spread-optionality.md diff --git a/.changeset/defer-named-spread-optionality.md b/.changeset/defer-named-spread-optionality.md new file mode 100644 index 00000000000..362e43b4405 --- /dev/null +++ b/.changeset/defer-named-spread-optionality.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-operations': patch +--- + +Keep deferred-only fields optional when a named `@defer` spread overlaps a field also selected outside the spread diff --git a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts index ff6e62ccf44..440135643d0 100644 --- a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts +++ b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts @@ -282,8 +282,11 @@ export class SelectionSetToObject< */ protected buildFragmentSpreadsUsage( spreads: FragmentSpreadNode[], - ): Record { - const selectionNodesByTypeName: Record = {}; + ): Record> { + const selectionNodesByTypeName: Record< + string, + Array + > = {}; for (const spread of spreads) { const fragmentSpreadObject = this._loadedFragments.find(lf => lf.name === spread.name.value); @@ -291,6 +294,8 @@ export class SelectionSetToObject< if (fragmentSpreadObject) { const schemaType = this._schema.getType(fragmentSpreadObject.onType); const possibleTypesForFragment = getPossibleTypes(this._schema, schemaType); + const fragmentDirectives = [...(spread.directives || [])]; + const isIncremental = hasIncrementalDeliveryDirectives(fragmentDirectives); for (const possibleType of possibleTypesForFragment) { const fragmentSuffix = this._getFragmentSuffix(spread.name.value); @@ -302,13 +307,40 @@ export class SelectionSetToObject< selectionNodesByTypeName[possibleType.name] ||= []; + // Expand @defer/@stream named spreads into per-selection nodes so each + // deferred field becomes its own optional union (same as inline @defer). + // Keeping them as one node collapses optionality when a field (e.g. `id`) + // is also selected outside the deferred spread. + // Skip when fragment masking is enabled — the Incremental<> fragment ref + // already models deferred optionality at the fragment boundary. + if (isIncremental && this._config.inlineFragmentTypes !== 'mask') { + for (const selection of fragmentSpreadObject.node.selectionSet.selections) { + if (selection.kind === Kind.FIELD) { + selectionNodesByTypeName[possibleType.name].push({ + ...selection, + fragmentDirectives, + }); + continue; + } + + selectionNodesByTypeName[possibleType.name].push({ + fragmentName: spread.name.value, + typeName: usage, + onType: fragmentSpreadObject.onType, + selectionNodes: [selection], + fragmentDirectives, + }); + } + continue; + } + const fragmentSelectionNodes: FragmentSpreadUsage['selectionNodes'] = [ ...fragmentSpreadObject.node.selectionSet.selections, ].map(originalNode => { if (originalNode.kind === Kind.FIELD) { return { ...originalNode, - fragmentDirectives: [...(spread.directives || [])], + fragmentDirectives, } satisfies EnrichedFieldNode; } return originalNode; @@ -319,7 +351,7 @@ export class SelectionSetToObject< typeName: usage, onType: fragmentSpreadObject.onType, selectionNodes: fragmentSelectionNodes, - fragmentDirectives: [...(spread.directives || [])], + fragmentDirectives, }); } } diff --git a/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts index baaf7c5d8c1..c89916841e3 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.skip-include-directives.spec.ts @@ -1109,12 +1109,12 @@ describe('TypeScript Operations Plugin - @include and @skip with @defer', () => "export type UserSkipQueryVariables = Exact<{ [key: string]: never; }>; - export type UserSkipQuery = { user: { id: string } & { name?: string, niName?: string } & { age?: number, createdAt?: string } & ({ age: number, createdAt: string } | { age?: never, createdAt?: never }) | null }; + export type UserSkipQuery = { user: { id: string } & { name?: string, niName?: string } & { age?: number, createdAt?: string } & ({ age?: number } | { age?: never }) & ({ createdAt?: string } | { createdAt?: never }) | null }; export type UserIncludeQueryVariables = Exact<{ [key: string]: never; }>; - export type UserIncludeQuery = { user: { id: string } & { name?: string, niName?: string } & { age?: number, createdAt?: string } & ({ age: number, createdAt: string } | { age?: never, createdAt?: never }) | null }; + export type UserIncludeQuery = { user: { id: string } & { name?: string, niName?: string } & { age?: number, createdAt?: string } & ({ age?: number } | { age?: never }) & ({ createdAt?: string } | { createdAt?: never }) | null }; export type User_NameFragment = { name: string, niName: string }; diff --git a/packages/plugins/typescript/operations/tests/ts-documents.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.spec.ts index f642e7aca03..61bc9f96e8b 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.spec.ts @@ -5586,7 +5586,7 @@ function test(q: GetEntityBrandDataQuery): void { export type UserQueryVariables = Exact<{ [key: string]: never; }>; - export type UserQuery = { user: { clearanceLevel: string, name: string, phone: { home: string }, employment: { title: string } } & ({ email: string } | { email?: never }) & ({ address: { street1: string } } | { address?: never }) & ({ widgetCount: number, widgetPreference: string } | { widgetCount?: never, widgetPreference?: never }) & ({ favoriteFood: string, leastFavoriteFood: string } | { favoriteFood?: never, leastFavoriteFood?: never }) }; + export type UserQuery = { user: { clearanceLevel: string, name: string, phone: { home: string }, employment: { title: string } } & ({ email: string } | { email?: never }) & ({ address: { street1: string } } | { address?: never }) & ({ widgetCount: number } | { widgetCount?: never }) & ({ widgetPreference: string } | { widgetPreference?: never }) & ({ favoriteFood: string } | { favoriteFood?: never }) & ({ leastFavoriteFood: string } | { leastFavoriteFood?: never }) }; " `); }); @@ -5678,7 +5678,7 @@ function test(q: GetEntityBrandDataQuery): void { export type UserQueryVariables = Exact<{ [key: string]: never; }>; - export type UserQuery = { user: { clearanceLevel: string, name: string, phone: { home: string }, employment: { title: string } } & ({ email: string } | { email?: never }) & ({ address: { street1: string | 'specialType' } } | { address?: never }) & ({ widgetName: string, widgetCount: number } | { widgetName?: never, widgetCount?: never }) }; + export type UserQuery = { user: { clearanceLevel: string, name: string, phone: { home: string }, employment: { title: string } } & ({ email: string } | { email?: never }) & ({ address: { street1: string | 'specialType' } } | { address?: never }) & ({ widgetName: string } | { widgetName?: never }) & ({ widgetCount: number } | { widgetCount?: never }) }; " `); }); @@ -5795,6 +5795,69 @@ function test(q: GetEntityBrandDataQuery): void { " `); }); + + it('keeps deferred-only fields optional when a named @defer spread overlaps a selected field', async () => { + // When `id` is selected both outside and inside a @defer named spread, bundling + // the spread as one union collapses optionality: intersecting with outer `id` + // eliminates the absent branch, so deferred-only fields become required. + // Each deferred selection must be its own optional union (same as inline @defer). + const schema = buildSchema(/* GraphQL */ ` + type User { + id: ID! + name: String! + connected: Boolean! + } + + type Query { + user: User! + } + `); + + const document = parse(/* GraphQL */ ` + fragment UserDeferred on User { + id + connected + } + + query user { + user { + id + name + ...UserDeferred @defer + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document }], + {}, + { outputFile: 'graphql.ts' }, + ); + + expect(content).toMatchInlineSnapshot(` + "export type UserDeferredFragment = { id: string, connected: boolean }; + + export type UserQueryVariables = Exact<{ [key: string]: never; }>; + + + export type UserQuery = { user: { id: string, name: string } & ({ id: string } | { id?: never }) & ({ connected: boolean } | { connected?: never }) }; + " + `); + + await validate( + content, + ` + declare function useUserQuery(): UserQuery; + const data = useUserQuery(); + // deferred-only field must remain optional despite overlapping \`id\` + const connected: boolean | undefined = data.user.connected; + // required non-deferred fields stay required + const id: string = data.user.id; + const name: string = data.user.name; + `, + ); + }); }); it('handles unnamed queries', async () => { From 546598d6d9dacb0f7c400e22c39654fdedbcc213 Mon Sep 17 00:00:00 2001 From: Sylvain Utard Date: Thu, 16 Jul 2026 10:39:24 +0200 Subject: [PATCH 2/3] fix(visitor-plugin-common): keep @defer optional on interfaces and nested spreads Preserve fragmentDirectives for interface/union @defer, promote nested @defer through inlined spreads, and restore trailing _ on implementing-type aliases when omitOperationSuffix is enabled. Co-authored-by: Cursor --- .changeset/defer-interface-inline-spread.md | 6 + .../src/selection-set-to-object.ts | 109 ++++++++++-- .../tests/extract-all-types.spec.ts | 8 +- .../operations/tests/ts-documents.spec.ts | 166 ++++++++++++++++++ 4 files changed, 275 insertions(+), 14 deletions(-) create mode 100644 .changeset/defer-interface-inline-spread.md diff --git a/.changeset/defer-interface-inline-spread.md b/.changeset/defer-interface-inline-spread.md new file mode 100644 index 00000000000..c800995405b --- /dev/null +++ b/.changeset/defer-interface-inline-spread.md @@ -0,0 +1,6 @@ +--- +'@graphql-codegen/visitor-plugin-common': patch +'@graphql-codegen/typescript-operations': patch +--- + +Keep `@defer` fields optional on interface/union selections, surface nested `@defer`/`@stream` through inlined fragment spreads, and restore trailing `_` on implementing-type aliases when `omitOperationSuffix` is enabled diff --git a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts index 440135643d0..46b41cb63a0 100644 --- a/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts +++ b/packages/plugins/other/visitor-plugin-common/src/selection-set-to-object.ts @@ -188,7 +188,9 @@ export class SelectionSetToObject< isInterfaceType(typeOnSchema) && parentType.getInterfaces().includes(typeOnSchema) ) { - this._appendToTypeMap(types, parentType.name, fields); + // Same as object-type path: keep inline-fragment directives on fields so + // @defer/@skip/@include survive the later `fragmentDirectives` filter. + this._appendToTypeMap(types, parentType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, parentType.name, spreadsUsage[parentType.name]); this._collectInlineFragments(typeOnSchema, inlines, types); } @@ -202,17 +204,22 @@ export class SelectionSetToObject< : parentType; const { fields, inlines, spreads } = separateSelectionSet(node.selectionSet.selections); const spreadsUsage = this.buildFragmentSpreadsUsage(spreads); + const directives = (node.directives as DirectiveNode[]) || undefined; + const fieldsWithFragmentDirectives: EnrichedFieldNode[] = fields.map(field => ({ + ...field, + fragmentDirectives: directives, + })); if ( isObjectType(schemaType) && possibleTypes.find(possibleType => possibleType.name === schemaType.name) ) { - this._appendToTypeMap(types, schemaType.name, fields); + this._appendToTypeMap(types, schemaType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, schemaType.name, spreadsUsage[schemaType.name]); this._collectInlineFragments(schemaType, inlines, types); } else if (isInterfaceType(schemaType) && schemaType.name === parentType.name) { for (const possibleType of possibleTypes) { - this._appendToTypeMap(types, possibleType.name, fields); + this._appendToTypeMap(types, possibleType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]); this._collectInlineFragments(schemaType, inlines, types); } @@ -228,7 +235,7 @@ export class SelectionSetToObject< // the field should only be added to the valid selections // in case the possible type actually implements the given interface if (isTypeSubTypeOf(this._schema, possibleType, fragmentSpreadType)) { - this._appendToTypeMap(types, possibleType.name, fields); + this._appendToTypeMap(types, possibleType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]); } } @@ -243,12 +250,17 @@ export class SelectionSetToObject< : parentType; const { fields, inlines, spreads } = separateSelectionSet(node.selectionSet.selections); const spreadsUsage = this.buildFragmentSpreadsUsage(spreads); + const directives = (node.directives as DirectiveNode[]) || undefined; + const fieldsWithFragmentDirectives: EnrichedFieldNode[] = fields.map(field => ({ + ...field, + fragmentDirectives: directives, + })); if ( isObjectType(schemaType) && possibleTypes.find(possibleType => possibleType.name === schemaType.name) ) { - this._appendToTypeMap(types, schemaType.name, fields); + this._appendToTypeMap(types, schemaType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, schemaType.name, spreadsUsage[schemaType.name]); this._collectInlineFragments(schemaType, inlines, types); } else if (isInterfaceType(schemaType)) { @@ -260,14 +272,14 @@ export class SelectionSetToObject< possibleInterfaceType => possibleInterfaceType.name === possibleType.name, ) ) { - this._appendToTypeMap(types, possibleType.name, fields); + this._appendToTypeMap(types, possibleType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]); this._collectInlineFragments(schemaType, inlines, types); } } } else { for (const possibleType of possibleTypes) { - this._appendToTypeMap(types, possibleType.name, fields); + this._appendToTypeMap(types, possibleType.name, fieldsWithFragmentDirectives); this._appendToTypeMap(types, possibleType.name, spreadsUsage[possibleType.name]); } } @@ -448,6 +460,24 @@ export class SelectionSetToObject< for (const [typeName, records] of Object.entries(fragmentSpreadsUsage)) { this._appendToTypeMap(result.selectionNodesByTypeName, typeName, records); + // Nested @defer/@stream inside spread fragments are otherwise dropped when + // `inlineFragmentTypes: 'inline'` re-flattens the spread (only the base map merges). + // Walk nested fragment defs with a visited set so types stay consistent and + // fragment cycles cannot hang codegen. (@skip/@include are handled elsewhere — + // promoting them here duplicates types under extractAllFieldsToTypesCompact.) + const nestedParentType = + this._schema.getType(typeName) ?? parentSchemaType ?? this._parentSchemaType; + for (const record of records) { + if (!('fragmentName' in record)) { + continue; + } + this._promoteNestedIncrementalSelections( + record.selectionNodes, + nestedParentType, + result, + new Set([record.fragmentName]), + ); + } } // 2. Push conditional inline fragments into the result.selectionNodesByTypeNameConditional @@ -494,6 +524,64 @@ export class SelectionSetToObject< } } + /** + * Surfaces @defer/@stream selections nested inside inlined fragment spreads onto the + * parent conditional map. `visitedFragmentNames` prevents cycles. + */ + private _promoteNestedIncrementalSelections( + selectionNodes: ReadonlyArray, + parentType: GraphQLNamedType, + result: ReturnType, + visitedFragmentNames: Set, + ): void { + for (const node of selectionNodes) { + if (!('kind' in node)) { + continue; + } + + if (node.kind === Kind.INLINE_FRAGMENT && hasIncrementalDeliveryDirectives(node.directives)) { + const conditionalNodes = new Map>(); + this._collectInlineFragments(parentType, [node], conditionalNodes); + result.selectionNodesByTypeNameConditional.push(conditionalNodes); + continue; + } + + if (node.kind !== Kind.FRAGMENT_SPREAD) { + continue; + } + + if (hasIncrementalDeliveryDirectives(node.directives)) { + const conditionalFragmentSpreadsUsage = this.buildFragmentSpreadsUsage([node]); + for (const [conditionalTypeName, conditionalRecords] of Object.entries( + conditionalFragmentSpreadsUsage, + )) { + const conditionalNodes = new Map>(); + this._appendToTypeMap(conditionalNodes, conditionalTypeName, conditionalRecords); + result.selectionNodesByTypeNameConditional.push(conditionalNodes); + } + continue; + } + + if (visitedFragmentNames.has(node.name.value)) { + continue; + } + visitedFragmentNames.add(node.name.value); + + const nestedFragment = this._loadedFragments.find(lf => lf.name === node.name.value); + if (!nestedFragment) { + continue; + } + + const nestedParentType = this._schema.getType(nestedFragment.onType) ?? parentType; + this._promoteNestedIncrementalSelections( + nestedFragment.node.selectionSet.selections, + nestedParentType, + result, + visitedFragmentNames, + ); + } + } + protected _buildGroupedSelections(parentName: string): { grouped: GroupedStringifiedTypes; dependentTypes: DependentType[]; @@ -1280,11 +1368,12 @@ export class SelectionSetToObject< } protected buildFragmentTypeName(name: string, suffix: string, typeName = ''): string { - const fragmentSuffix = - typeName && suffix ? `_${typeName}_${suffix}` : typeName ? `_${typeName}` : suffix; + // Keep `_${typeName}_${suffix}` even when `omitOperationSuffix` makes `suffix` ''. + // The intermediate `typeName && suffix ? … : typeName ? …` form dropped the trailing + // `_` on implementing-type aliases (`Foo_Bar` instead of `Foo_Bar_`). return this._convertName(name, { useTypesPrefix: true, - suffix: fragmentSuffix, + suffix: typeName ? `_${typeName}_${suffix}` : suffix, }); } diff --git a/packages/plugins/typescript/operations/tests/extract-all-types.spec.ts b/packages/plugins/typescript/operations/tests/extract-all-types.spec.ts index ad36055f450..81c90925514 100644 --- a/packages/plugins/typescript/operations/tests/extract-all-types.spec.ts +++ b/packages/plugins/typescript/operations/tests/extract-all-types.spec.ts @@ -81,21 +81,21 @@ describe('extractAllFieldsToTypes: true', () => { { outputFile: '' }, ); expect(content).toMatchInlineSnapshot(` - "type UserFragment_DummyUser = { + "type UserFragment_DummyUser_ = { __typename: 'DummyUser', id: string, joinDate: unknown }; - type UserFragment_ActiveUser = { + type UserFragment_ActiveUser_ = { __typename: 'ActiveUser', id: string, joinDate: unknown }; export type UserFragment = - | UserFragment_DummyUser - | UserFragment_ActiveUser + | UserFragment_DummyUser_ + | UserFragment_ActiveUser_ ; export type MeFragment_ActiveUser_parentUser_DummyUser = { diff --git a/packages/plugins/typescript/operations/tests/ts-documents.spec.ts b/packages/plugins/typescript/operations/tests/ts-documents.spec.ts index 61bc9f96e8b..bd0fd98dbf7 100644 --- a/packages/plugins/typescript/operations/tests/ts-documents.spec.ts +++ b/packages/plugins/typescript/operations/tests/ts-documents.spec.ts @@ -5858,6 +5858,172 @@ function test(q: GetEntityBrandDataQuery): void { `, ); }); + + it('keeps @defer fields optional on interface selections', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface EventStats { + id: ID! + name: String! + trends: Int! + } + + type ConcreteEventStats implements EventStats { + id: ID! + name: String! + trends: Int! + } + + type Query { + event: EventStats! + } + `); + + const document = parse(/* GraphQL */ ` + fragment EventCard on EventStats { + id + name + ... @defer { + id + trends + } + } + + query event { + event { + ...EventCard + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document }], + { inlineFragmentTypes: 'inline' }, + { outputFile: 'graphql.ts' }, + ); + + expect(content).toContain('({ trends: number } | { trends?: never })'); + expect(content).not.toMatch(/trends: number,/); + + await validate( + content, + ` + declare function useEventQuery(): EventQuery; + const data = useEventQuery(); + const trends: number | undefined = data.event.trends; + const name: string = data.event.name; + `, + ); + }); + + it('surfaces nested @defer fields when inlining a fragment spread', async () => { + const schema = buildSchema(/* GraphQL */ ` + type Connection { + id: ID! + name: String! + connected: Boolean! + } + + type Query { + connection: Connection! + } + `); + + const document = parse(/* GraphQL */ ` + fragment ConnectionStatus on Connection { + id + ... @defer { + id + connected + } + } + + fragment ConnectionRow on Connection { + id + name + ...ConnectionStatus + } + + query connection { + connection { + ...ConnectionRow + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document }], + { inlineFragmentTypes: 'inline' }, + { outputFile: 'graphql.ts' }, + ); + + expect(content).toContain('({ connected: boolean } | { connected?: never })'); + + await validate( + content, + ` + declare function useConnectionQuery(): ConnectionQuery; + const data = useConnectionQuery(); + const connected: boolean | undefined = data.connection.connected; + const name: string = data.connection.name; + `, + ); + }); + + it('keeps trailing underscore on implementing-type aliases with omitOperationSuffix', async () => { + const schema = buildSchema(/* GraphQL */ ` + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String! + } + + type Post implements Node { + id: ID! + title: String! + } + + type Query { + node: Node! + } + `); + + const document = parse(/* GraphQL */ ` + fragment NodeCard on Node { + id + ... on User { + name + } + ... on Post { + title + } + } + + query node { + node { + ...NodeCard + } + } + `); + + const { content } = await plugin( + schema, + [{ location: '', document }], + { omitOperationSuffix: true }, + { outputFile: 'graphql.ts' }, + ); + + // With omitOperationSuffix, implementing-type aliases historically ended with `_` + // (`NodeCard_User_`). The v7 formula dropped that trailing underscore. + expect(content).toContain('NodeCard_User_'); + expect(content).toContain('NodeCard_Post_'); + expect(content).not.toMatch(/export type NodeCard_User =/); + expect(content).not.toMatch(/export type NodeCard_Post =/); + }); }); it('handles unnamed queries', async () => { From e195bc9de7b63725a2bb3b02d6bacce44eb8748a Mon Sep 17 00:00:00 2001 From: Sylvain Utard Date: Thu, 16 Jul 2026 10:40:41 +0200 Subject: [PATCH 3/3] chore: merge defer-related changesets into one entry Co-authored-by: Cursor --- .changeset/defer-interface-inline-spread.md | 6 ------ .changeset/defer-named-spread-optionality.md | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 .changeset/defer-interface-inline-spread.md diff --git a/.changeset/defer-interface-inline-spread.md b/.changeset/defer-interface-inline-spread.md deleted file mode 100644 index c800995405b..00000000000 --- a/.changeset/defer-interface-inline-spread.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@graphql-codegen/visitor-plugin-common': patch -'@graphql-codegen/typescript-operations': patch ---- - -Keep `@defer` fields optional on interface/union selections, surface nested `@defer`/`@stream` through inlined fragment spreads, and restore trailing `_` on implementing-type aliases when `omitOperationSuffix` is enabled diff --git a/.changeset/defer-named-spread-optionality.md b/.changeset/defer-named-spread-optionality.md index 362e43b4405..da540081812 100644 --- a/.changeset/defer-named-spread-optionality.md +++ b/.changeset/defer-named-spread-optionality.md @@ -3,4 +3,4 @@ '@graphql-codegen/typescript-operations': patch --- -Keep deferred-only fields optional when a named `@defer` spread overlaps a field also selected outside the spread +Keep `@defer` fields optional: expand overlapping named `@defer` spreads into per-selection unions, preserve optionality on interface/union selections, surface nested `@defer`/`@stream` through inlined fragment spreads, and restore trailing `_` on implementing-type aliases when `omitOperationSuffix` is enabled