From 66b585c16f371634ec6805b0f2fbf4a8b6037ae0 Mon Sep 17 00:00:00 2001 From: Phil Pluckthun Date: Mon, 22 Apr 2024 11:38:51 +0100 Subject: [PATCH] fix: Ensure TS excess property error can be triggered by `graphql.scalar` (#243) --- .changeset/nervous-rice-invent.md | 5 +++ src/__tests__/api.test-d.ts | 52 +++++++++++++++++++++++++++++++ src/api.ts | 36 ++++++++++++++++----- src/variables.ts | 24 +++----------- 4 files changed, 91 insertions(+), 26 deletions(-) create mode 100644 .changeset/nervous-rice-invent.md diff --git a/.changeset/nervous-rice-invent.md b/.changeset/nervous-rice-invent.md new file mode 100644 index 00000000..33c966ee --- /dev/null +++ b/.changeset/nervous-rice-invent.md @@ -0,0 +1,5 @@ +--- +"gql.tada": patch +--- + +Ensure that `graphql.scalar()` accepts value arguments as-is, so excess properties trigger an error. diff --git a/src/__tests__/api.test-d.ts b/src/__tests__/api.test-d.ts index e22597b6..c33bfe1f 100644 --- a/src/__tests__/api.test-d.ts +++ b/src/__tests__/api.test-d.ts @@ -146,6 +146,29 @@ describe('graphql()', () => { }; }>(); }); + + it('should preserve object literal types for variables', () => { + const mutation = graphql(` + mutation ($input: TodoPayload!) { + updateTodo(input: $input) { + id + } + } + `); + + expectTypeOf>().toEqualTypeOf<{ + input: { + title: string; + description: string; + complete?: boolean | null | undefined; + }; + }>(); + + const vars = (input: VariablesOf) => input; + + // @ts-expect-error + vars({ excess: true, input: { title: 'title', description: 'description' } }); + }); }); describe('graphql() with custom scalars', () => { @@ -310,6 +333,16 @@ describe('graphql.scalar()', () => { expectTypeOf().toEqualTypeOf(); }); + it('should return the type of a given input object', () => { + type actual = ReturnType>; + + expectTypeOf().toEqualTypeOf<{ + complete?: boolean | undefined | null; + title: string; + description: string; + }>(); + }); + it('should return the type of a given enum', () => { type actual = ReturnType>; expectTypeOf().toEqualTypeOf(); @@ -335,6 +368,25 @@ describe('graphql.scalar()', () => { // @ts-expect-error const actual = graphql.scalar('what', null); }); + + it('should accept exact input objects', () => { + const actual = graphql.scalar('TodoPayload', { + title: 'title', + description: 'description', + }); + + expectTypeOf().toEqualTypeOf<{ + title: string; + description: string; + }>(); + + graphql.scalar('TodoPayload', { + title: 'title', + description: 'description', + // @ts-expect-error + excess: true, + }); + }); }); describe('graphql.scalar() with custom scalars', () => { diff --git a/src/api.ts b/src/api.ts index 53706372..da2fda59 100644 --- a/src/api.ts +++ b/src/api.ts @@ -176,17 +176,39 @@ interface GraphQLTadaAPI, - >( + scalar( name: Name, - value: Value + value: getScalarType extends Value + ? Value & getScalarType + : getScalarType ): Value; - + scalar( + name: Name, + value: getScalarType extends Value + ? never extends Value + ? never + : (Value & getScalarType) | null + : getScalarType | null + ): Value | null; + scalar( + name: Name, + value: getScalarType extends Value + ? never extends Value + ? never + : (Value & getScalarType) | undefined + : getScalarType | undefined + ): Value | undefined; + scalar( + name: Name, + value: getScalarType extends Value + ? never extends Value + ? never + : (Value & getScalarType) | null | undefined + : getScalarType | null | undefined + ): Value | null | undefined; scalar( name: Name, - value?: getScalarType + value: getScalarType | undefined ): getScalarType; /** Function to replace a GraphQL document with a persisted document. diff --git a/src/variables.ts b/src/variables.ts index 684c8a3d..ea5c2a87 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -37,8 +37,8 @@ type unwrapTypeRec = Type : null | Array> : TypeRef extends { name: any } ? IsOptional extends false - ? _getScalarType - : null | _getScalarType + ? getScalarType + : null | getScalarType : unknown; type unwrapTypeRefRec = Type extends { @@ -52,8 +52,8 @@ type unwrapTypeRefRec = Type : null | Array> : Type extends { kind: Kind.NAMED_TYPE; name: any } ? IsOptional extends false - ? _getScalarType - : null | _getScalarType + ? getScalarType + : null | getScalarType : unknown; type _getVariablesRec< @@ -90,7 +90,7 @@ type getVariablesType< Introspection extends SchemaLike, > = _getVariablesRec; -type _getScalarType< +type getScalarType< TypeName, Introspection extends SchemaLike, > = TypeName extends keyof Introspection['types'] @@ -99,20 +99,6 @@ type _getScalarType< : Introspection['types'][TypeName] extends { type: any } ? Introspection['types'][TypeName]['type'] : Introspection['types'][TypeName]['enumValues'] - : unknown; - -type getScalarType< - TypeName, - Introspection extends SchemaLike, - OrType = never, -> = TypeName extends keyof Introspection['types'] - ? Introspection['types'][TypeName] extends { kind: 'INPUT_OBJECT'; inputFields: any } - ? getInputObjectTypeRec | OrType - : Introspection['types'][TypeName] extends { type: any } - ? Introspection['types'][TypeName]['type'] | OrType - : Introspection['types'][TypeName] extends { enumValues: any } - ? Introspection['types'][TypeName]['enumValues'] | OrType - : never : never; export type { getVariablesType, getScalarType };