Implement fragment unmasking utility types
Description
Implementation of fragment unmasking to support an incremental adoption of fragment masking or utils for tests.
When you enable Fragment Masking, all fragments and operations are automatically masked, even though not all fragments or operations use Fragment Masking.
We can expose the following utility types: UnmaskResultOf<TypedDocumentNode> & UnmaskFragmentType<TType>.
It checks whether we're inside an operation or directly within a fragment and then recursively flattens fragments by resolving $fragmentRefs and merging them to the root fragment.
const FilmItemFragment = graphql(`
fragment FilmItem on Film {
id
title
}
`);
const AllFilmsFragment = graphql(`
fragment AllFilms on Root {
allFilms {
films {
...FilmItem
}
}
}
`);
const myQuery = graphql(`
query Films {
...AllFilms
}
`); // DocumentTypeDecoration<R, V>
// Fragment references are all inlined rather than referring to ` $fragmentRefs`.
type UnmaskedData = UnmaskResultOf<typeof myQuery>;
// ^? type UnmaskedData = {
// __typename: 'Root' | undefined,
// allFilms: {
// films: {
// __typename?: "Film" | undefined;
// id: string;
// title?: string | null | undefined;
// }[] | null | undefined
// } | null | undefined
// }
Related #9075
I also introduced expect-type to test those newly generated TypeScript utilities to prevent regressions. Currently, there's no way to test it before generating fragment-masking.ts as definitions are living within strings, so we need to generate examples and then rely on a Jest test within apollo-client's example combined with expect-type to test them.
Type of change
- [x] New feature (non-breaking change which adds functionality)
- [x] This change requires a documentation update
Screenshots/Sandbox (if appropriate/relevant):
How Has This Been Tested?
- [x] Added tests to cover TypeScript utility types
- [x] TypeScript Playground
- [x] I have followed the CONTRIBUTING doc and the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules
🦋 Changeset detected
Latest commit: f18151a427a71b62dced5d9f726f023c5ca067cb
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 1 package
| Name | Type |
|---|---|
| @graphql-codegen/client-preset | Minor |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
@charpeni just wanted to provide feedback as our team attempted to utilize these proposed types in our application. In our case, any fragment over 2 to 3 levels deep caused widespread TS2589: Type instantiation is excessively deep and possibly infinite..
@brandonwestcott Do you believe it's something that could be reproduced on TypeScript Playground?
Not directly related to GraphQL Code Generator, but I noticed some weird behaviors starting with TypeScript 5.0 and worse with 5.1 related to Type instantiation is excessively deep and possibly infinite when it's not excessively deep at all.
I tried to reproduce it on TypeScript Playground with one query and five nested fragments, and I couldn't: 🔗 TypeScript Playground.
But! Weirdly enough, I was able to reproduce it when trying to unmask a type that couldn't be resolved: 🔗 TypeScript Playground.
I like the idea a lot. But here's my problem: I'm in Vue, using bindings from @urql/vue (useQuery, useFragment and so on). Since the types proposed here accept as input the result from a graphql('...') function call, I'd have to
const rawQuery = graphql(`...`);
const queryResData = useQuery({
query: rawQuery
}).data as UnmaskResultOf<typeof rawQuery>
which is quite entangled. I'd love to be able to write a function unmaskAllFragments, which would be a no-op only doing the necessary assertion. To be used like
const queryRes = unmaskAllFragments(useQuery({
query: graphql(`...`)
});
This would require the generic type accepted by UnmaskResultOf to be something other than DocumentTypeDecoration, because that is no longer present in the result of useQuery (but, all the necessary information should still be there, since I can "manually" unmask the fragment with useFragment).
I'll dig around a bit more, maybe I'll find a way to achieve this, but I wouldn't bet on it ;)
Just to note, the utility type doesn't work on fragments applied to nested objects of the query, which my team unfortunately depends on.
Adapted your implementation to take it one step further, and I've tested for:
- nested fragments / sibling fragments
- fragments in nested objects
- fragments in arrays
- fragments in optional fields
The implementation can be found here.
No more updates? I would like to use this utility type too. I will try to implement it in another PR if there is no further update