#3538·urql

RFC: Graphcache strong typing

Author: BickelLukasCreated Mar 19, 2024Updated Apr 11, 2024
Labelsfuture 🔮

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

typescript
import { graphql } from "./graphql";
import { introspection } from "./introspection";
import { CacheExchangeOpts, UpdateResolver } from "@urql/exchange-graphcache";
import { GraphQLTadaAPI } from "gql.tada";

type Introspection = typeof introspection;
type IntrospectionTypes = {
  [K in Introspection["__schema"]["types"][number]["name"]]: Introspection["__schema"]["types"][number] extends infer Type
    ? Type extends {
        readonly name: K;
      }
      ? Type extends IntrospectionObjectType
        ? mapObject<Type>
        : Type
      : never
    : never;
};
type IntrospectionMutations = IntrospectionTypes[Introspection["__schema"]["mutationType"]["name"]]["fields"];

type ExtractSchema<T> = T extends GraphQLTadaAPI<infer S, any> ? S : never;
type Schema = ExtractSchema<typeof graphql>;

type Mutations = Schema["types"][Schema["mutation"]]["fields"];
type Subscriptions = Schema["types"][Schema["subscription"]]["fields"];

type getResultType<Name extends string, T> = { [K in Name]: unwrapTypeRec<T, Schema, true> };
type getVariablesType<T> = T extends keyof IntrospectionMutations ? getInputObjectTypeRec<IntrospectionMutations[T]["args"], Schema> : never;

export type TypedCacheExchangeOpts = CacheExchangeOpts & {
  updates: {
    Mutation: {
      [key in keyof Mutations]?: UpdateResolver<getResultType<key, Mutations[key]["type"]>, getVariablesType<Mutations[key]["name"]>>;
    };
    Subscription: {
      [key in keyof Subscriptions]?: UpdateResolver<getResultType<key, Subscriptions[key]["type"]>, getVariablesType<Subscriptions[key]["name"]>>;
    };
  };
};

/* Copied and slightly modified from gql-tada */
interface IntrospectionObjectType {
  readonly kind: "OBJECT";
  readonly name: string;
  readonly fields: readonly any[];
}
interface IntrospectionField {
  readonly name: string;
  readonly type: IntrospectionTypeRef;
  readonly args: readonly any[];
}
interface IntrospectionNamedTypeRef {
  readonly name: string;
}
interface IntrospectionListTypeRef {
  readonly kind: "LIST";
  readonly ofType: IntrospectionTypeRef;
}
interface IntrospectionNonNullTypeRef {
  readonly kind: "NON_NULL";
  readonly ofType: IntrospectionTypeRef;
}
type IntrospectionTypeRef = IntrospectionNamedTypeRef | IntrospectionListTypeRef | IntrospectionNonNullTypeRef;

type IntrospectionLikeType = {
  query: string;
  mutation?: any;
  subscription?: any;
  types: {
    [name: string]: any;
  };
};
type mapField<T> = T extends IntrospectionField
  ? {
      name: T["name"];
      args: T["args"];
    }
  : never;
type mapObject<T extends IntrospectionObjectType> = {
  kind: "OBJECT";
  name: T["name"];
  fields: obj<{
    [P in T["fields"][number]["name"]]: T["fields"][number] extends infer Field
      ? Field extends {
          readonly name: P;
        }
        ? mapField<Field>
        : never
      : never;
  }>;
  args: true;
};

type obj<T> = T extends {
  [key: string | number]: any;
}
  ? {
      [K in keyof T]: T[K];
    }
  : never;

type getInputObjectTypeRec<InputFields, Introspection extends IntrospectionLikeType, InputObject = {}> = InputFields extends readonly [
  infer InputField,
  ...infer Rest,
]
  ? getInputObjectTypeRec<
      Rest,
      Introspection,
      (InputField extends {
        name: any;
        type: any;
      }
        ? InputField extends {
            defaultValue?: undefined | null;
            type: {
              kind: "NON_NULL";
            };
          }
          ? {
              [Name in InputField["name"]]: unwrapTypeRec<InputField["type"], Introspection, true>;
            }
          : {
              [Name in InputField["name"]]?: unwrapTypeRec<InputField["type"], Introspection, true> | null;
            }
        : {}) &
        InputObject
    >
  : InputObject;
type unwrapTypeRec<TypeRef, Introspection extends IntrospectionLikeType, IsOptional> = TypeRef extends {
  kind: "NON_NULL";
  ofType: any;
}
  ? unwrapTypeRec<TypeRef["ofType"], Introspection, false>
  : TypeRef extends {
        kind: "LIST";
        ofType: any;
      }
    ? IsOptional extends false
      ? Array<unwrapTypeRec<TypeRef["ofType"], Introspection, true>>
      : null | Array<unwrapTypeRec<TypeRef["ofType"], Introspection, true>>
    : TypeRef extends {
          name: any;
        }
      ? IsOptional extends false
        ? _getScalarType<TypeRef["name"], Introspection>
        : null | _getScalarType<TypeRef["name"], Introspection>
      : unknown;
type _getScalarType<TypeName, Introspection extends IntrospectionLikeType> = TypeName extends keyof Introspection["types"]
  ? Introspection["types"][TypeName] extends {
      kind: "SCALAR" | "ENUM";
      type: any;
    }
    ? Introspection["types"][TypeName]["type"]
    : Introspection["types"][TypeName] extends {
          kind: "INPUT_OBJECT";
          inputFields: any;
        }
      ? obj<getInputObjectTypeRec<Introspection["types"][TypeName]["inputFields"], Introspection>>
      : Introspection["types"][TypeName] extends {
            kind: "OBJECT";
            fields: any;
          }
        ? getObjectTypeRec<Introspection["types"][TypeName]["fields"], Introspection>
        : never
  : unknown;

type getObjectTypeRec<Fields, Introspection extends IntrospectionLikeType> = Fields extends {
  [key: string]: { name: string; type: any };
}
  ? {
      [K in keyof Fields]?: unwrapTypeRec<Fields[K]["type"], Introspection, true>;
    }
  : never;