From 6b4c0145d76dab57e5186fdd6d4d94ca5b77fbd2 Mon Sep 17 00:00:00 2001 From: Jovi De Croock Date: Tue, 21 Nov 2023 12:01:19 +0100 Subject: [PATCH] helper to derive fragment types (#6) --- src/__tests__/fragments.test-d.ts | 31 +++++++++++++++++++++++++++++++ src/typed-document/fragments.ts | 23 +++++++++++++++++++++++ src/typed-document/index.ts | 4 ++-- 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/fragments.test-d.ts create mode 100644 src/typed-document/fragments.ts diff --git a/src/__tests__/fragments.test-d.ts b/src/__tests__/fragments.test-d.ts new file mode 100644 index 00000000..678c03d0 --- /dev/null +++ b/src/__tests__/fragments.test-d.ts @@ -0,0 +1,31 @@ +import { assertType, test } from 'vitest'; +import { Introspection } from '../introspection'; +import { Document } from '../parser'; +import { FragmentType } from '../typed-document/fragments'; +import { schema } from './introspection.test-d'; + +type Intro = Introspection; +const any = {} as any; + +test('creates a type for a given fragment', () => { + const unionQuery = ` + fragment Fields on Todo { + id + text + complete + __typename + } +`; + + type doc = Document; + type typedDoc = FragmentType; + + const actual = any as typedDoc; + + assertType<{ + id: string | number; + text: string; + complete: boolean | null; + __typename: 'Todo'; + }>(actual); +}); diff --git a/src/typed-document/fragments.ts b/src/typed-document/fragments.ts new file mode 100644 index 00000000..ca9e1354 --- /dev/null +++ b/src/typed-document/fragments.ts @@ -0,0 +1,23 @@ +import type { FragmentDefinitionNode, Kind } from '@0no-co/graphql.web'; +import type { Selection, ObjectLikeType, FragmentMap } from './index'; +import type { Introspection as IntrospectionType } from '../introspection'; + +export type FragmentType< + Document extends { kind: Kind.DOCUMENT; definitions: any[] }, + Introspection extends IntrospectionType, + Fragments extends Record = FragmentMap +> = Document['definitions'][0] extends { + kind: Kind.FRAGMENT_DEFINITION; + typeCondition: { name: { value: infer TypeName } }; +} + ? TypeName extends keyof Introspection['types'] + ? Introspection['types'][TypeName] extends ObjectLikeType + ? Selection< + Document['definitions'][0]['selectionSet']['selections'], + Introspection['types'][TypeName], + Introspection, + Fragments + > + : never + : never + : never; diff --git a/src/typed-document/index.ts b/src/typed-document/index.ts index 4e7b77e2..de8cc92b 100644 --- a/src/typed-document/index.ts +++ b/src/typed-document/index.ts @@ -20,7 +20,7 @@ import type { } from '../introspection'; import type { Obj, ObjValues } from '../utils'; -type ObjectLikeType = { +export type ObjectLikeType = { kind: 'OBJECT' | 'INTERFACE' | 'UNION'; name: string; fields: { [key: string]: IntrospectionField }; @@ -119,7 +119,7 @@ type TypenameOfType = X extends { // i.e. excluding `PossibleFragmentsSelection<...>['__typename']` when we're on a GraphQL union or interface? // TODO: For the interface case, do we need to type-union `FieldSelectionContinue<...>` into each possible // intersection type of `PossibleFragmentsSelection<...>`? -type Selection< +export type Selection< Selections extends readonly any[], Type extends ObjectLikeType, Introspection extends IntrospectionType,