Skip to content

[typescript-operations] Fix @skip and @include not applying conditional modifiers correctly when used on inline fragment#10645

Merged
eddeee888 merged 7 commits intomaster-nextfrom
master-next-fix-skip-include
Mar 16, 2026
Merged

[typescript-operations] Fix @skip and @include not applying conditional modifiers correctly when used on inline fragment#10645
eddeee888 merged 7 commits intomaster-nextfrom
master-next-fix-skip-include

Conversation

@eddeee888
Copy link
Copy Markdown
Collaborator

@eddeee888 eddeee888 commented Mar 16, 2026

Description

Previously, all inline fragment fields were merged with the base selection set, without considering if conditional directives (@skip or @inlcude) are applied on at the inline fragment, all fields of said fragment are optional as well

Now, conditional inline fragments are separated from the base and other inline fragments so correct conditional modifiers can be applied to the fields.

For example, if we have the following query:

query User {
  user {
    id # base selection set 
    ... on User { # non-conditional inline fragment
      name
    }
    ... on User @include(if: true) { # conditional inline fragment
      createdAt
    }
  }
}

Then, the generated Result type would look like this:

type UserQuery = { 
  user: { 
    name: string; // this comes from the non-conditional inline fragment and merged with the base type
    id: string // this comes from the base selection set
  } & {
    createdAt?: string // because this is part of a conditional spread, its fields are conditional as well
  }
}

Related: #10496
Issue: #9745

Type of change

  • Bug fix (non-breaking change which fixes an issue)

How Has This Been Tested?

  • Unit test

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 16, 2026

🦋 Changeset detected

Latest commit: f1be5d6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@graphql-codegen/typescript-operations Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@eddeee888 eddeee888 changed the title Master next fix skip include [typescript-operations] Fix @skip and @include not applying conditional modifiers correctly to inline fragment fields Mar 16, 2026
@eddeee888 eddeee888 changed the title [typescript-operations] Fix @skip and @include not applying conditional modifiers correctly to inline fragment fields [typescript-operations] Fix @skip and @include not applying conditional modifiers correctly when used on inline fragment Mar 16, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 16, 2026

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
@graphql-codegen/cli 7.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/introspection 5.0.2-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/visitor-plugin-common 7.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-document-nodes 5.0.10-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/gql-tag-operations 5.1.5-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-operations 6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript-resolvers 6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/typed-document-node 6.1.8-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/typescript 6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/client-preset 6.0.0-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎
@graphql-codegen/graphql-modules-preset 5.1.5-alpha-20260316123536-f1be5d641cea639e270924f1ec243d48c90bda21 npm ↗︎ unpkg ↗︎

Comment on lines -71 to +86
type CollectedFragmentNode = (SelectionNode | FragmentSpreadUsage | DirectiveNode) & FragmentDirectives;
/**
* Each grouped TypeName has an array of nodes.
* These are collected when parsing the selection set,
* then turned into TypeScript strings
*/
type GroupedTypeNameNode =
| EnrichedFieldNode
| FragmentSpreadNode
| InlineFragmentNode
| FragmentSpreadUsage
| DirectiveNode;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GroupedTypeNameNode is a replacement for CollectedFragmentNode to be more semantically and type correct:

  • Semantic improvement: CollectedFragmentNode is not the correct name since we collect certain nodes, not just Fragments.
  • Type improvement: previously, DirectiveNode can never have fragmentDirectives field, so that was misleading that it has & FragmentDirectives. Now, only FieldNode (becomes EnrichedFieldNode) and FragmentSpreadNode may have fragmentDirectives field

Comment on lines +82 to +84
| EnrichedFieldNode
| FragmentSpreadNode
| InlineFragmentNode
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SelectionNode type is FieldNode | FragmentSpreadNode | InlineFragmentNode.

Since a FieldNode here could be enriched with fragmentDirectives (directives from the fragment the field is in), so I've just inlined and replaced FieldNode with EnrichedFieldNode

Comment on lines -235 to -254
protected _createInlineFragmentForFieldNodes(
parentType: GraphQLNamedType,
fieldNodes: FieldNode[]
): InlineFragmentNode {
return {
kind: Kind.INLINE_FRAGMENT,
typeCondition: {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: parentType.name,
},
},
directives: [],
selectionSet: {
kind: Kind.SELECTION_SET,
selections: fieldNodes,
},
};
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just inlined below, since it's only used once

return result;
}

private _appendToTypeMap<T = CollectedFragmentNode>(
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never use the generic, may as well inline it.

Comment on lines +432 to +496
const collectGrouped = (nodes: GroupedTypeNameNode[]): void => {
// incrementalNodes are the ones flagged with @defer, meaning they become nullable
const { selectionNodes, incrementalNodes, fragmentSpreads } = nodes.reduce<{
selectionNodes: GroupedTypeNameNode[];
incrementalNodes: FragmentSpreadUsage[];
fragmentSpreads: string[];
}>(
(acc, node) => {
if ('fragmentDirectives' in node && hasIncrementalDeliveryDirectives(node.fragmentDirectives)) {
acc.incrementalNodes.push(node as FragmentSpreadUsage); // FIXME: check whether @defer would pick up EnrichedFieldNode here too?
} else {
acc.selectionNodes.push(node);
}
return acc;
},
{ selectionNodes: [], incrementalNodes: [], fragmentSpreads: [] }
);

const { fields, dependentTypes: subDependentTypes } = this.buildSelectionSet(schemaType, selectionNodes, {
parentFieldName: this.buildParentFieldName(typeName, parentName),
});
const transformedSet = this.selectionSetStringFromFields(fields);
const { fields, dependentTypes: subDependentTypes } = this.buildSelectionSet(schemaType, selectionNodes, {
parentFieldName: this.buildParentFieldName(typeName, parentName),
});
const transformedSet = this.selectionSetStringFromFields(fields);

if (transformedSet) {
prev[typeName].push(transformedSet);
}
dependentTypes.push(...subDependentTypes);
if (!transformedSet && !fragmentSpreads.length) {
mustAddEmptyObject = true;
}
if (transformedSet) {
prev[typeName].push(transformedSet);
}
dependentTypes.push(...subDependentTypes);
if (!transformedSet && !fragmentSpreads.length) {
mustAddEmptyObject = true;
}

for (const incrementalNode of incrementalNodes) {
// 1. fragment masking
if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) {
const { fields: incrementalFields, dependentTypes: incrementalDependentTypes } = this.buildSelectionSet(
for (const incrementalNode of incrementalNodes) {
// 1. fragment masking
if (this._config.inlineFragmentTypes === 'mask' && 'fragmentName' in incrementalNode) {
const { fields: incrementalFields, dependentTypes: incrementalDependentTypes } = this.buildSelectionSet(
schemaType,
[incrementalNode],
{ unsetTypes: true, parentFieldName: parentName }
);
const incrementalSet = this.selectionSetStringFromFields(incrementalFields);
prev[typeName].push(incrementalSet);
dependentTypes.push(...incrementalDependentTypes);

continue;
}

// 2. @defer
const { fields: initialFields, dependentTypes: initialDependentTypes } = this.buildSelectionSet(
schemaType,
[incrementalNode],
{ parentFieldName: parentName }
);

const { fields: subsequentFields, dependentTypes: subsequentDependentTypes } = this.buildSelectionSet(
schemaType,
[incrementalNode],
{ unsetTypes: true, parentFieldName: parentName }
);
const incrementalSet = this.selectionSetStringFromFields(incrementalFields);
prev[typeName].push(incrementalSet);
dependentTypes.push(...incrementalDependentTypes);

continue;
const initialSet = this.selectionSetStringFromFields(initialFields);
const subsequentSet = this.selectionSetStringFromFields(subsequentFields);
dependentTypes.push(...initialDependentTypes, ...subsequentDependentTypes);
prev[typeName].push({ union: [initialSet, subsequentSet] });
}
};
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, this function only runs on the base selection set and inline fragments.
This is now extracted to run on conditional fragments as well.

Comment on lines +603 to +604
const primitiveFields = new Map<string, EnrichedFieldNode>();
const primitiveAliasFields = new Map<string, EnrichedFieldNode>();
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each FieldNode in this function may have fragmentDirectives in it. This makes it more type correct

// 1. Handle Field or Directtives selection nodes
if ('kind' in selectionNode) {
if (selectionNode.kind === 'Field') {
if (selectionNode.kind === Kind.FIELD) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Kind improves consistency here

Comment on lines -122 to -124
export type FragmentDirectives = {
fragmentDirectives?: Array<DirectiveNode>;
};
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to selection-set-to-object.ts as it's only used there

Comment on lines +502 to +507
/**
* Check if any of the directives are conditional i.e. `@skip` and `@include`
*/
export function hasConditionalDirectives(directives: readonly DirectiveNode[] = []): boolean {
const CONDITIONAL_DIRECTIVES = ['skip', 'include'];
return field.directives?.some(directive => CONDITIONAL_DIRECTIVES.includes(directive.name.value));
return directives.some(directive => CONDITIONAL_DIRECTIVES.includes(directive.name.value));
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasConditionalDirectives is updated to handle an array of directives, instead of just FieldNode for more flexibilities.

Directives may appear on a more nodes than just fields.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved tests related to @skip and @include to ts-documents.skip-include-directives.spec.ts as-is

@eddeee888 eddeee888 merged commit 81afcbe into master-next Mar 16, 2026
19 checks passed
@eddeee888 eddeee888 deleted the master-next-fix-skip-include branch March 16, 2026 13:20
eddeee888 added a commit that referenced this pull request Mar 19, 2026
…tional modifiers correctly when used on inline fragment (#10645)

* Migrate skip and include directive tests to a new file

* Add test for aliased field

* Add test for include directive, inline fragment cases

* Refactor selection set to object to reflect current usages

* Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used

* Add inline fragment skip directive test

* Add changeset
eddeee888 added a commit that referenced this pull request Apr 13, 2026
…tional modifiers correctly when used on inline fragment (#10645)

* Migrate skip and include directive tests to a new file

* Add test for aliased field

* Add test for include directive, inline fragment cases

* Refactor selection set to object to reflect current usages

* Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used

* Add inline fragment skip directive test

* Add changeset
eddeee888 added a commit that referenced this pull request Apr 18, 2026
…tional modifiers correctly when used on inline fragment (#10645)

* Migrate skip and include directive tests to a new file

* Add test for aliased field

* Add test for include directive, inline fragment cases

* Refactor selection set to object to reflect current usages

* Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used

* Add inline fragment skip directive test

* Add changeset
eddeee888 added a commit that referenced this pull request Apr 19, 2026
…tional modifiers correctly when used on inline fragment (#10645)

* Migrate skip and include directive tests to a new file

* Add test for aliased field

* Add test for include directive, inline fragment cases

* Refactor selection set to object to reflect current usages

* Standardise type, parsing, fragment directive handling and fix conditional when inline fragment is used

* Add inline fragment skip directive test

* Add changeset
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

Successfully merging this pull request may close these issues.

1 participant