Skip to content

Commit

Permalink
patch: Match optional (T | {}) fragment data (#349)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitten authored Jul 29, 2024
1 parent f9c9861 commit 0d80e95
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/quiet-panthers-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gql.tada": patch
---

Extend `readFragment` types to allow `| {}` optional fragments to be matched. When a fragment is annotated with a directive making it optional (such as `@include`, `@skip`, or `@defer`) then its typed as optional. `readFragment` previously didn't know how to match these types, but it will now match `T | {}` and infer the type as such.
124 changes: 124 additions & 0 deletions src/__tests__/api.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,130 @@ describe('readFragment', () => {
const result = readFragment({} as document, {} as FragmentOf<document>);
expectTypeOf<typeof result>().toEqualTypeOf<ResultOf<document>>();
});

it('should unmask nullable, undefined, and optional data', () => {
type fragment = parseDocument<`
fragment Fields on Todo @_unmask {
id
}
`>;

type document = getDocumentNode<fragment, schema>;

const inputA: FragmentOf<document> | null = {} as any;
const resultA = readFragment({} as document, inputA);
expectTypeOf<typeof resultA>().toEqualTypeOf<ResultOf<document> | null>();

const inputB: FragmentOf<document> | undefined = {} as any;
const resultB = readFragment({} as document, inputB);
expectTypeOf<typeof resultB>().toEqualTypeOf<ResultOf<document> | undefined>();

const inputC: FragmentOf<document> | undefined | null = {} as any;
const resultC = readFragment({} as document, inputC);
expectTypeOf<typeof resultC>().toEqualTypeOf<ResultOf<document> | undefined | null>();

const inputD: FragmentOf<document> | {} = {} as any;
const resultD = readFragment({} as document, inputD);
expectTypeOf<typeof resultD>().toEqualTypeOf<ResultOf<document> | {}>();

const inputE: FragmentOf<document> | {} | null = {} as any;
const resultE = readFragment({} as document, inputE);
expectTypeOf<typeof resultE>().toEqualTypeOf<ResultOf<document> | {} | null>();
});

it('should unmask arrays of nullable, undefined, and optional data', () => {
type fragment = parseDocument<`
fragment Fields on Todo @_unmask {
id
}
`>;

type document = getDocumentNode<fragment, schema>;

const inputA: (FragmentOf<document> | null)[] = [];
const resultA = readFragment({} as document, inputA);
expectTypeOf<typeof resultA>().toEqualTypeOf<readonly (ResultOf<document> | null)[]>();

const inputB: (FragmentOf<document> | undefined)[] = [];
const resultB = readFragment({} as document, inputB);
expectTypeOf<typeof resultB>().toEqualTypeOf<readonly (ResultOf<document> | undefined)[]>();

const inputC: (FragmentOf<document> | undefined | null)[] = [];
const resultC = readFragment({} as document, inputC);
expectTypeOf<typeof resultC>().toEqualTypeOf<
readonly (ResultOf<document> | undefined | null)[]
>();

const inputD: (FragmentOf<document> | {})[] = [];
const resultD = readFragment({} as document, inputD);
expectTypeOf<typeof resultD>().toEqualTypeOf<readonly (ResultOf<document> | {})[]>();

const inputE: (FragmentOf<document> | {} | null)[] = [];
const resultE = readFragment({} as document, inputE);
expectTypeOf<typeof resultE>().toEqualTypeOf<readonly (ResultOf<document> | {} | null)[]>();
});

it('should unmask nullable, undefined, and optional data (with passed generic)', () => {
type fragment = parseDocument<`
fragment Fields on Todo @_unmask {
id
}
`>;

type document = getDocumentNode<fragment, schema>;

const inputA: FragmentOf<document> | null = {} as any;
const resultA = readFragment<document>(inputA);
expectTypeOf<typeof resultA>().toEqualTypeOf<ResultOf<document> | null>();

const inputB: FragmentOf<document> | undefined = {} as any;
const resultB = readFragment<document>(inputB);
expectTypeOf<typeof resultB>().toEqualTypeOf<ResultOf<document> | undefined>();

const inputC: FragmentOf<document> | undefined | null = {} as any;
const resultC = readFragment<document>(inputC);
expectTypeOf<typeof resultC>().toEqualTypeOf<ResultOf<document> | undefined | null>();

const inputD: FragmentOf<document> | {} = {} as any;
const resultD = readFragment<document>(inputD);
expectTypeOf<typeof resultD>().toEqualTypeOf<ResultOf<document> | {}>();

const inputE: FragmentOf<document> | {} | null = {} as any;
const resultE = readFragment<document>(inputE);
expectTypeOf<typeof resultE>().toEqualTypeOf<ResultOf<document> | {} | null>();
});

it('should unmask arrays of nullable, undefined, and optional data (with passed generic)', () => {
type fragment = parseDocument<`
fragment Fields on Todo @_unmask {
id
}
`>;

type document = getDocumentNode<fragment, schema>;

const inputA: (FragmentOf<document> | null)[] = [];
const resultA = readFragment<document>(inputA);
expectTypeOf<typeof resultA>().toEqualTypeOf<readonly (ResultOf<document> | null)[]>();

const inputB: (FragmentOf<document> | undefined)[] = [];
const resultB = readFragment<document>(inputB);
expectTypeOf<typeof resultB>().toEqualTypeOf<readonly (ResultOf<document> | undefined)[]>();

const inputC: (FragmentOf<document> | undefined | null)[] = [];
const resultC = readFragment<document>(inputC);
expectTypeOf<typeof resultC>().toEqualTypeOf<
readonly (ResultOf<document> | undefined | null)[]
>();

const inputD: (FragmentOf<document> | {})[] = [];
const resultD = readFragment<document>(inputD);
expectTypeOf<typeof resultD>().toEqualTypeOf<readonly (ResultOf<document> | {})[]>();

const inputE: (FragmentOf<document> | {} | null)[] = [];
const resultE = readFragment<document>(inputE);
expectTypeOf<typeof resultE>().toEqualTypeOf<readonly (ResultOf<document> | {} | null)[]>();
});
});

describe('maskFragments', () => {
Expand Down
82 changes: 50 additions & 32 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,17 +512,27 @@ type fragmentRefsOfFragmentsRec<
* @see {@link readFragment} for how to read from fragment masks.
*/
function readFragment<const Document extends FragmentShape = never>(
_document: Document,
fragment: resultOrFragmentOf<Document>
): ResultOf<Document>;
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | null
): ResultOf<Document> | null;
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | undefined
): ResultOf<Document> | undefined;
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | null | undefined
): ResultOf<Document> | null | undefined;
// Reading fragments where input data is an array and nullable
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
function readFragment<
const Document extends FragmentShape,
const T extends resultOrFragmentOf<Document> | null | undefined | {},
>(
_document: Document,
fragment: T
): T extends resultOrFragmentOf<Document> ? ResultOf<Document> : T;

// Reading arrays of fragments with required generic
function readFragment<const Document extends FragmentShape = never>(
fragment: readonly resultOrFragmentOf<Document>[]
): readonly ResultOf<Document>[];
Expand All @@ -535,38 +545,46 @@ function readFragment<const Document extends FragmentShape = never>(
function readFragment<const Document extends FragmentShape = never>(
fragment: readonly (resultOrFragmentOf<Document> | null | undefined)[]
): readonly (ResultOf<Document> | null | undefined)[];
// Reading arrays of fragments with required generic with optional `{}` type
function readFragment<const Document extends FragmentShape = never>(
fragment: readonly (resultOrFragmentOf<Document> | {})[]
): readonly (ResultOf<Document> | {})[];
function readFragment<const Document extends FragmentShape = never>(
fragment: readonly (resultOrFragmentOf<Document> | null | {})[]
): readonly (ResultOf<Document> | null | {})[];
function readFragment<const Document extends FragmentShape = never>(
fragment: readonly (resultOrFragmentOf<Document> | undefined | {})[]
): readonly (ResultOf<Document> | undefined | {})[];
function readFragment<const Document extends FragmentShape = never>(
fragment: readonly (resultOrFragmentOf<Document> | null | undefined | {})[]
): readonly (ResultOf<Document> | null | undefined | {})[];
// Reading fragments with required generic
function readFragment<const Document extends FragmentShape = never>(
_document: Document,
fragment: resultOrFragmentOf<Document>
): ResultOf<Document>;
function readFragment<const Document extends FragmentShape>(
_document: Document,
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | null
): ResultOf<Document> | null;
function readFragment<const Document extends FragmentShape>(
_document: Document,
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | undefined
): ResultOf<Document> | undefined;
function readFragment<const Document extends FragmentShape>(
_document: Document,
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | null | undefined
): ResultOf<Document> | null | undefined;
function readFragment<const Document extends FragmentShape>(
_document: Document,
fragment: readonly resultOrFragmentOf<Document>[]
): readonly ResultOf<Document>[];
function readFragment<const Document extends FragmentShape>(
_document: Document,
fragment: readonly (resultOrFragmentOf<Document> | null)[]
): readonly (ResultOf<Document> | null)[];
function readFragment<const Document extends FragmentShape>(
_document: Document,
fragment: readonly (resultOrFragmentOf<Document> | undefined)[]
): readonly (ResultOf<Document> | undefined)[];
function readFragment<const Document extends FragmentShape>(
_document: Document,
fragment: readonly (resultOrFragmentOf<Document> | null | undefined)[]
): readonly (ResultOf<Document> | null | undefined)[];
// Reading fragments with required generic with optional `{}` type
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | {}
): ResultOf<Document> | {};
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | null | {}
): ResultOf<Document> | null | {};
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | undefined | {}
): ResultOf<Document> | undefined | {};
function readFragment<const Document extends FragmentShape = never>(
fragment: resultOrFragmentOf<Document> | null | undefined | {}
): ResultOf<Document> | null | undefined | {};

function readFragment(...args: [unknown] | [unknown, unknown]) {
return args.length === 2 ? args[1] : args[0];
}
Expand Down

0 comments on commit 0d80e95

Please sign in to comment.