Open BickelLukas opened 3 months ago
This is definitely something we intend to include in gql.tada
. The only issue I can foresee is that the introspection output there isn't 100% stabilised, and we plan to actually output the pre-formatted version of it as well.
So, if we write up a version that's targeting the current output in gql.tada
(from mapIntrospection
) that'd likely help with performance of inference in the future.
There's also some old issues to revisit with the typings and how they're being consumed by Graphcache, but that's likely TBD until we explore this again.
But yea, I'd currently favour absorbing the complexity here into a gql.tada
sub-module (under maybe gql.tada/addons/graphcache
maybe)
cc @JoviDeCroock
Including it in gql.tada would definitely be a step in the right direction. But I was thinking since graphcache already supports a schma aware mode, this could be implemented with any schema that is supplied to graphcache, regardless of where it comes from. This would decouple it completely from gql.tata and make it compatible with other graphql type generators
I figured I'd add what I've been patching into my current project as it might be useful in further implementing this feature.
When using Cache.resolve
, I noticed that sometimes a scalar value is returned and other times a reference is returned if the result would have been an object due to the cache normalizing data. It also seemed like it would be nice if there was type-aware autocompletion regarding the field
parameter of the resolve function as well, so I came up with the following:
import type { DataField, Entity, FieldArgs } from '@urql/exchange-graphcache';
/**
* This utility type represents a reference returned from `Cache.resolve`,
* typically looking like `Book:123` but with the full type being referenced
* stored alongside it in branded-type fashion
*/
type Reference<T> = string & { __def: T };
/**
* This utility type is used to "normalize" an object type and return it
* as either a reference type or a scalar.
*/
type Normalize<T> =
T extends Array<infer U>
? Array<Normalize<U>>
: T extends object // Check if T can be normalized
? Reference<T> // If it can, return the normalized reference type
: T; // If it doesn't, return T
// Example
type Book = {
__typename: 'Book';
id: string;
title: string;
publicationDate: string;
author: {
__typename: 'Author';
id: string;
name: string;
};
};
type BookReference = Reference<Book>;
// ▲ string & { __def: Book; }
/**
* This helper casts any `Entity` to a reference type, essentially prepping it
* for use in `Cache.resolve` without actually modifying its underlying type.
*/
const toReference = <T>(entity: Entity): Reference<T> => entity as Reference<T>;
/**
* This utility type is used to resolve a reference type to its full type
*/
type Resolved<T extends Reference<any>> = T extends Reference<infer U> ? U : T;
@urql/exchange-graphcache
Type OverrideBy overriding the definition of the Cache.resolve
function, I am able to
hook into the type E
of the entity
parameter, which then gives me access to the underlying type being obfuscated by Reference
. This can then be leveraged to add autocompletion of the field
parameter by limiting it to properties that exist on the resolved type of E
. The return type will be the normalized value of the property accessed via F
, which will either be a scalar or another Reference
.
declare module '@urql/exchange-graphcache' {
interface Cache {
// Custom typing
resolve<E extends Reference<any>, F extends keyof Resolved<E>>(
entity: E | undefined,
field: F, // You get type completion here
args?: FieldArgs,
): Normalize<Resolved<E>[F]>;
// Original typing
resolve(entity: Entity | undefined, fieldName: string, args?: FieldArgs): DataField | undefined;
}
}
Within an optimistic update:
type Book = {
__typename: 'Book';
id: string;
title: string;
publicationDate: string;
author: {
__typename: 'Author';
id: string;
name: string;
};
};
const cache = cacheExchange({
optimistic: {
updateBook: (args, cache) => {
const bookRef = toReference<Book>({ __typename: 'Book', id: args.id });
// ▲ Reference<Book>
return {
__typename: 'Book',
id: args.id,
title: args.title,
author: () => {
// ▼ intellisense limits this to keyof Book
const authorRef = cache.resolve(bookRef, 'author');
// ▲ Reference<{
// __typename: 'Author';
// id: string;
// name: string;
// }>
return {
__typename: 'Author',
// ▼ intellisense limits this to '__typename' | 'id' | 'name'
id: cache.resolve(authorRef, 'id'), // string
// ▼ intellisense limits this to '__typename' | 'id' | 'name'
name: () => cache.resolve(authorRef, 'name'), // string
};
},
};
},
},
});
Summary
Currently all the operations on the graphcache are completely untyped. There is a old graphql-codegen project to support typing but that is apparently unmaintained: https://github.com/urql-graphql/urql/discussions/2323
Taking the lessons learned from https://gql-tada.0no.co/ it should be possible to add typings to graphcache operations when an introspection schema is provided.
Proposed Solution
I have already played around and come up with a proof of concept that works using the instrospection created by 0no-co/graphqlsp