Skip to content

Commit

Permalink
fix: Add type narrowing to readFragment (#372)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten committed Aug 18, 2024
1 parent 8849b97 commit 5328c8a
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-taxis-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gql.tada": patch
---

Allow `readFragment(doc, data)` to narrow output's `__typename`s by the input type.
77 changes: 77 additions & 0 deletions src/__tests__/api.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,83 @@ describe('readFragment', () => {
type document = getDocumentNode<fragment, schema>;
const result = readFragment({} as document, {} as FragmentOf<document>);
expectTypeOf<typeof result>().toEqualTypeOf<ResultOf<document>>();

const narrowInput = {} as FragmentOf<document> & { __typename?: 'BigTodo' };
const narrowed = readFragment({} as document, narrowInput);
expectTypeOf<typeof narrowed>().toEqualTypeOf<{
__typename?: 'BigTodo';
id: unknown;
wallOfText: unknown;
}>();
});

it('unmasks fragments of interfaces while narrowing types using input', () => {
type fragment = parseDocument<`
fragment Fields on ITodo {
id
... on BigTodo {
wallOfText
}
... on SmallTodo {
maxLength
}
}
`>;

type document = getDocumentNode<fragment, schema>;

const data: FragmentOf<document> & { __typename?: 'SmallTodo' } = {} as any;
const result = readFragment({} as document, data);
expectTypeOf<typeof result>().toEqualTypeOf<{
__typename?: 'SmallTodo';
id: unknown;
maxLength: unknown;
}>();
});

it('should allow for gradual narrowing', () => {
type childFragment = parseDocument<`
fragment Fields on ITodo {
id
... on BigTodo {
wallOfText
}
... on SmallTodo {
maxLength
}
}
`>;

type parentFragment = parseDocument<`
fragment Parent on ITodo {
__typename
...Fields
}
`>;

type childFragmentDoc = getDocumentNode<childFragment, schema>;
type parentFragmentDoc = getDocumentNode<
parentFragment,
schema,
getFragmentsOfDocuments<[childFragmentDoc]>
>;

const input: ResultOf<parentFragmentDoc> = {} as any;
if (input.__typename === 'SmallTodo') {
const result = readFragment({} as childFragmentDoc, input);
expectTypeOf<typeof result>().toEqualTypeOf<{
__typename?: 'SmallTodo';
id: unknown;
maxLength: unknown;
}>();
} else if (input.__typename === 'BigTodo') {
const result = readFragment({} as childFragmentDoc, input);
expectTypeOf<typeof result>().toEqualTypeOf<{
__typename?: 'BigTodo';
id: unknown;
wallOfText: unknown;
}>();
}
});

it('unmasks fragments of interfaces with optional spreads', () => {
Expand Down
22 changes: 16 additions & 6 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,17 @@ type FragmentOf<Document extends FragmentShape> = makeFragmentRef<Document>;

type resultOrFragmentOf<Document extends FragmentShape> = FragmentOf<Document> | ResultOf<Document>;

type resultOfT<Document extends FragmentShape, T = unknown> = Document extends DocumentDecoration<
infer Result,
any
>
? '__typename' extends keyof T
? Result extends { __typename?: T['__typename'] }
? Result
: never
: Result
: never;

type resultOfFragmentsRec<
Fragments extends readonly any[],
Result = {},
Expand Down Expand Up @@ -513,24 +524,23 @@ type fragmentRefsOfFragmentsRec<
*/
function readFragment<const Document extends FragmentShape = never>(
_document: Document,
fragment: resultOrFragmentOf<Document>
): ResultOf<Document>;
// Reading fragments where input data is an array and nullable
fragment: never
): resultOfT<Document>;

function readFragment<
const Document extends FragmentShape,
const T extends resultOrFragmentOf<Document> | null | undefined | {},
>(
_document: Document,
fragments: readonly T[]
): readonly (T extends resultOrFragmentOf<Document> ? ResultOf<Document> : T)[];
// Reading fragments where input data is nullable
): readonly (T extends resultOrFragmentOf<Document> ? resultOfT<Document, T> : T)[];
function readFragment<
const Document extends FragmentShape,
const T extends resultOrFragmentOf<Document> | null | undefined | {},
>(
_document: Document,
fragment: T
): T extends resultOrFragmentOf<Document> ? ResultOf<Document> : T;
): T extends resultOrFragmentOf<Document> ? resultOfT<Document, T> : T;

// Reading arrays of fragments with required generic
function readFragment<const Document extends FragmentShape>(
Expand Down

0 comments on commit 5328c8a

Please sign in to comment.