Skip to content

Commit 5328c8a

Browse files
authored
fix: Add type narrowing to readFragment (#372)
1 parent 8849b97 commit 5328c8a

File tree

3 files changed

+98
-6
lines changed

3 files changed

+98
-6
lines changed

.changeset/ten-taxis-shop.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gql.tada": patch
3+
---
4+
5+
Allow `readFragment(doc, data)` to narrow output's `__typename`s by the input type.

src/__tests__/api.test-d.ts

+77
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,83 @@ describe('readFragment', () => {
578578
type document = getDocumentNode<fragment, schema>;
579579
const result = readFragment({} as document, {} as FragmentOf<document>);
580580
expectTypeOf<typeof result>().toEqualTypeOf<ResultOf<document>>();
581+
582+
const narrowInput = {} as FragmentOf<document> & { __typename?: 'BigTodo' };
583+
const narrowed = readFragment({} as document, narrowInput);
584+
expectTypeOf<typeof narrowed>().toEqualTypeOf<{
585+
__typename?: 'BigTodo';
586+
id: unknown;
587+
wallOfText: unknown;
588+
}>();
589+
});
590+
591+
it('unmasks fragments of interfaces while narrowing types using input', () => {
592+
type fragment = parseDocument<`
593+
fragment Fields on ITodo {
594+
id
595+
... on BigTodo {
596+
wallOfText
597+
}
598+
... on SmallTodo {
599+
maxLength
600+
}
601+
}
602+
`>;
603+
604+
type document = getDocumentNode<fragment, schema>;
605+
606+
const data: FragmentOf<document> & { __typename?: 'SmallTodo' } = {} as any;
607+
const result = readFragment({} as document, data);
608+
expectTypeOf<typeof result>().toEqualTypeOf<{
609+
__typename?: 'SmallTodo';
610+
id: unknown;
611+
maxLength: unknown;
612+
}>();
613+
});
614+
615+
it('should allow for gradual narrowing', () => {
616+
type childFragment = parseDocument<`
617+
fragment Fields on ITodo {
618+
id
619+
... on BigTodo {
620+
wallOfText
621+
}
622+
... on SmallTodo {
623+
maxLength
624+
}
625+
}
626+
`>;
627+
628+
type parentFragment = parseDocument<`
629+
fragment Parent on ITodo {
630+
__typename
631+
...Fields
632+
}
633+
`>;
634+
635+
type childFragmentDoc = getDocumentNode<childFragment, schema>;
636+
type parentFragmentDoc = getDocumentNode<
637+
parentFragment,
638+
schema,
639+
getFragmentsOfDocuments<[childFragmentDoc]>
640+
>;
641+
642+
const input: ResultOf<parentFragmentDoc> = {} as any;
643+
if (input.__typename === 'SmallTodo') {
644+
const result = readFragment({} as childFragmentDoc, input);
645+
expectTypeOf<typeof result>().toEqualTypeOf<{
646+
__typename?: 'SmallTodo';
647+
id: unknown;
648+
maxLength: unknown;
649+
}>();
650+
} else if (input.__typename === 'BigTodo') {
651+
const result = readFragment({} as childFragmentDoc, input);
652+
expectTypeOf<typeof result>().toEqualTypeOf<{
653+
__typename?: 'BigTodo';
654+
id: unknown;
655+
wallOfText: unknown;
656+
}>();
657+
}
581658
});
582659

583660
it('unmasks fragments of interfaces with optional spreads', () => {

src/api.ts

+16-6
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,17 @@ type FragmentOf<Document extends FragmentShape> = makeFragmentRef<Document>;
449449

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

452+
type resultOfT<Document extends FragmentShape, T = unknown> = Document extends DocumentDecoration<
453+
infer Result,
454+
any
455+
>
456+
? '__typename' extends keyof T
457+
? Result extends { __typename?: T['__typename'] }
458+
? Result
459+
: never
460+
: Result
461+
: never;
462+
452463
type resultOfFragmentsRec<
453464
Fragments extends readonly any[],
454465
Result = {},
@@ -513,24 +524,23 @@ type fragmentRefsOfFragmentsRec<
513524
*/
514525
function readFragment<const Document extends FragmentShape = never>(
515526
_document: Document,
516-
fragment: resultOrFragmentOf<Document>
517-
): ResultOf<Document>;
518-
// Reading fragments where input data is an array and nullable
527+
fragment: never
528+
): resultOfT<Document>;
529+
519530
function readFragment<
520531
const Document extends FragmentShape,
521532
const T extends resultOrFragmentOf<Document> | null | undefined | {},
522533
>(
523534
_document: Document,
524535
fragments: readonly T[]
525-
): readonly (T extends resultOrFragmentOf<Document> ? ResultOf<Document> : T)[];
526-
// Reading fragments where input data is nullable
536+
): readonly (T extends resultOrFragmentOf<Document> ? resultOfT<Document, T> : T)[];
527537
function readFragment<
528538
const Document extends FragmentShape,
529539
const T extends resultOrFragmentOf<Document> | null | undefined | {},
530540
>(
531541
_document: Document,
532542
fragment: T
533-
): T extends resultOrFragmentOf<Document> ? ResultOf<Document> : T;
543+
): T extends resultOrFragmentOf<Document> ? resultOfT<Document, T> : T;
534544

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

0 commit comments

Comments
 (0)