Skip to content

RFC: Graphcache strong typing #3538

Description

@BickelLukas

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: #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

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;

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    future 🔮An enhancement or feature proposal that will be addressed after the next release

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions