Skip to content

Commit

Permalink
fix(gql.tada): Fix recursive optional fragment types causing exponent…
Browse files Browse the repository at this point in the history
…ial complexity (#319)
  • Loading branch information
kitten authored Jun 13, 2024
1 parent 8f2ca2b commit 49e9b78
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/hungry-needles-rule.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 7 additions & 12 deletions src/__tests__/selection.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -149,19 +150,13 @@ test('infers optional fragment for @defer', () => {
}
`>;

type actual = getDocumentType<query, schema>;
type actual = obj<
(getDocumentType<query, schema>['todos'] extends infer U | null ? U : never)[number]
>;

type expected = {
todos: Array<
| {
id: string;
}
| {}
| null
> | null;
};
type expected = { id: string } | {};

expectTypeOf<expected>().toEqualTypeOf<actual>();
expectTypeOf<actual>().toMatchTypeOf<expected>();
});

test('infers optional inline fragment for @defer', () => {
Expand Down Expand Up @@ -489,7 +484,7 @@ test('creates a type for a given fragment with optional inline spread', () => {
}
`>;

type actual = getDocumentType<fragment, schema>;
type actual = obj<getDocumentType<fragment, schema>>;

type expected =
| {}
Expand Down
104 changes: 63 additions & 41 deletions src/selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -100,11 +99,10 @@ type getFragmentSelection<
PossibleType,
Type,
Introspection,
Fragments,
{}
Fragments
>
: {}
: {};
: never
: never;

type getSpreadSubtype<
Node,
Expand Down Expand Up @@ -143,66 +141,90 @@ 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<Selections, Type['name'], Type, Introspection, Fragments, {}>
? getPossibleTypeSelectionRec<Selections, Type['name'], Type, Introspection, Fragments>
: {};

interface typeSelectionResult<Fields extends {} = {}, Rest = unknown> {
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,
PossibleType,
Type,
Introspection,
Fragments,
(Node extends { kind: Kind.FRAGMENT_SPREAD | Kind.INLINE_FRAGMENT }
Node extends { kind: Kind.FRAGMENT_SPREAD | Kind.INLINE_FRAGMENT }
? getSpreadSubtype<Node, Type, Introspection, Fragments> extends infer Subtype extends
ObjectLikeType
? PossibleType extends getTypenameOfType<Subtype>
?
| (isOptional<Node> extends true ? {} : never)
| getFragmentSelection<Node, PossibleType, Subtype, Introspection, Fragments>
: {}
? isOptional<Node> extends true
? typeSelectionResult<
SelectionAcc['fields'],
SelectionAcc['rest'] &
(
| {}
| getFragmentSelection<Node, PossibleType, Subtype, Introspection, Fragments>
)
>
: typeSelectionResult<
SelectionAcc['fields'] &
getFragmentSelection<Node, PossibleType, Subtype, Introspection, Fragments>,
SelectionAcc['rest']
>
: SelectionAcc
: Node extends { kind: Kind.FRAGMENT_SPREAD; name: any }
? makeUndefinedFragmentRef<Node['name']['value']>
: {}
? typeSelectionResult<
SelectionAcc['fields'] & makeUndefinedFragmentRef<Node['name']['value']>,
SelectionAcc['rest']
>
: SelectionAcc
: Node extends { kind: Kind.FIELD; name: any; selectionSet: any }
? isOptional<Node> extends true
? {
[Prop in getFieldAlias<Node>]?: Node['name']['value'] extends '__typename'
? PossibleType
: unwrapTypeRec<
Type['fields'][Node['name']['value']]['type'],
Node['selectionSet'],
Introspection,
Fragments,
getTypeDirective<Node>
>;
}
: {
[Prop in getFieldAlias<Node>]: Node['name']['value'] extends '__typename'
? PossibleType
: unwrapTypeRec<
Type['fields'][Node['name']['value']]['type'],
Node['selectionSet'],
Introspection,
Fragments,
getTypeDirective<Node>
>;
}
: {}) &
SelectionAcc
? typeSelectionResult<
SelectionAcc['fields'] & {
[Prop in getFieldAlias<Node>]?: Node['name']['value'] extends '__typename'
? PossibleType
: unwrapTypeRec<
Type['fields'][Node['name']['value']]['type'],
Node['selectionSet'],
Introspection,
Fragments,
getTypeDirective<Node>
>;
},
SelectionAcc['rest']
>
: typeSelectionResult<
SelectionAcc['fields'] & {
[Prop in getFieldAlias<Node>]: Node['name']['value'] extends '__typename'
? PossibleType
: unwrapTypeRec<
Type['fields'][Node['name']['value']]['type'],
Node['selectionSet'],
Introspection,
Fragments,
getTypeDirective<Node>
>;
},
SelectionAcc['rest']
>
: SelectionAcc
>
: obj<SelectionAcc>;
: obj<SelectionAcc['fields']> & SelectionAcc['rest'];

type getOperationSelectionType<
Definition,
Expand Down

0 comments on commit 49e9b78

Please sign in to comment.