diff --git a/.changeset/hungry-needles-rule.md b/.changeset/hungry-needles-rule.md new file mode 100644 index 00000000..f81738ce --- /dev/null +++ b/.changeset/hungry-needles-rule.md @@ -0,0 +1,5 @@ +--- +"gql.tada": patch +--- + +Fix `@defer`, `@skip`, and `@include` optional fragments causing types to become exponentially more complex to evaluate, causing a recursive type error. Instead, merging field types and sub-selections from fragments is now separated, as needed. diff --git a/src/__tests__/selection.test-d.ts b/src/__tests__/selection.test-d.ts index 17441400..990d5f4c 100644 --- a/src/__tests__/selection.test-d.ts +++ b/src/__tests__/selection.test-d.ts @@ -4,6 +4,7 @@ import type { simpleSchema } from './fixtures/simpleSchema'; import type { parseDocument } from '../parser'; import type { mapIntrospection, addIntrospectionScalars } from '../introspection'; import type { getDocumentType } from '../selection'; +import type { obj } from '../utils'; import type { $tada, @@ -149,19 +150,13 @@ test('infers optional fragment for @defer', () => { } `>; - type actual = getDocumentType; + type actual = obj< + (getDocumentType['todos'] extends infer U | null ? U : never)[number] + >; - type expected = { - todos: Array< - | { - id: string; - } - | {} - | null - > | null; - }; + type expected = { id: string } | {}; - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toMatchTypeOf(); }); test('infers optional inline fragment for @defer', () => { @@ -489,7 +484,7 @@ test('creates a type for a given fragment with optional inline spread', () => { } `>; - type actual = getDocumentType; + type actual = obj>; type expected = | {} diff --git a/src/selection.ts b/src/selection.ts index 225e368a..882e90f9 100644 --- a/src/selection.ts +++ b/src/selection.ts @@ -88,8 +88,7 @@ type getFragmentSelection< PossibleType, Type, Introspection, - Fragments, - {} + Fragments > : Node extends { kind: Kind.FRAGMENT_SPREAD; name: any } ? Node['name']['value'] extends keyof Fragments @@ -100,11 +99,10 @@ type getFragmentSelection< PossibleType, Type, Introspection, - Fragments, - {} + Fragments > - : {} - : {}; + : never + : never; type getSpreadSubtype< Node, @@ -143,20 +141,25 @@ type getSelection< // - Marking the field as optional makes it clear that it cannot just be used // - It protects against a very specific edge case where users forget to select `__typename` // above and below an unmasked fragment, causing TypeScript to show unmergeable types - { __typename?: PossibleType } + typeSelectionResult<{ __typename?: PossibleType }> >; }[Type['possibleTypes']] : Type extends { kind: 'OBJECT'; name: any } - ? getPossibleTypeSelectionRec + ? getPossibleTypeSelectionRec : {}; +interface typeSelectionResult { + fields: Fields; + rest: Rest; +} + type getPossibleTypeSelectionRec< Selections, PossibleType extends string, Type extends ObjectLikeType, Introspection extends SchemaLike, Fragments extends { [name: string]: any }, - SelectionAcc, + SelectionAcc extends typeSelectionResult = typeSelectionResult, > = Selections extends [infer Node, ...infer Rest] ? getPossibleTypeSelectionRec< Rest, @@ -164,45 +167,64 @@ type getPossibleTypeSelectionRec< Type, Introspection, Fragments, - (Node extends { kind: Kind.FRAGMENT_SPREAD | Kind.INLINE_FRAGMENT } + Node extends { kind: Kind.FRAGMENT_SPREAD | Kind.INLINE_FRAGMENT } ? getSpreadSubtype extends infer Subtype extends ObjectLikeType ? PossibleType extends getTypenameOfType - ? - | (isOptional extends true ? {} : never) - | getFragmentSelection - : {} + ? isOptional extends true + ? typeSelectionResult< + SelectionAcc['fields'], + SelectionAcc['rest'] & + ( + | {} + | getFragmentSelection + ) + > + : typeSelectionResult< + SelectionAcc['fields'] & + getFragmentSelection, + SelectionAcc['rest'] + > + : SelectionAcc : Node extends { kind: Kind.FRAGMENT_SPREAD; name: any } - ? makeUndefinedFragmentRef - : {} + ? typeSelectionResult< + SelectionAcc['fields'] & makeUndefinedFragmentRef, + SelectionAcc['rest'] + > + : SelectionAcc : Node extends { kind: Kind.FIELD; name: any; selectionSet: any } ? isOptional extends true - ? { - [Prop in getFieldAlias]?: Node['name']['value'] extends '__typename' - ? PossibleType - : unwrapTypeRec< - Type['fields'][Node['name']['value']]['type'], - Node['selectionSet'], - Introspection, - Fragments, - getTypeDirective - >; - } - : { - [Prop in getFieldAlias]: Node['name']['value'] extends '__typename' - ? PossibleType - : unwrapTypeRec< - Type['fields'][Node['name']['value']]['type'], - Node['selectionSet'], - Introspection, - Fragments, - getTypeDirective - >; - } - : {}) & - SelectionAcc + ? typeSelectionResult< + SelectionAcc['fields'] & { + [Prop in getFieldAlias]?: Node['name']['value'] extends '__typename' + ? PossibleType + : unwrapTypeRec< + Type['fields'][Node['name']['value']]['type'], + Node['selectionSet'], + Introspection, + Fragments, + getTypeDirective + >; + }, + SelectionAcc['rest'] + > + : typeSelectionResult< + SelectionAcc['fields'] & { + [Prop in getFieldAlias]: Node['name']['value'] extends '__typename' + ? PossibleType + : unwrapTypeRec< + Type['fields'][Node['name']['value']]['type'], + Node['selectionSet'], + Introspection, + Fragments, + getTypeDirective + >; + }, + SelectionAcc['rest'] + > + : SelectionAcc > - : obj; + : obj & SelectionAcc['rest']; type getOperationSelectionType< Definition,