Skip to content

Commit

Permalink
fix: Ensure TS excess property error can be triggered by `graphql.sca…
Browse files Browse the repository at this point in the history
…lar` (#243)
  • Loading branch information
kitten authored Apr 22, 2024
1 parent 5888a0c commit 66b585c
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/nervous-rice-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gql.tada": patch
---

Ensure that `graphql.scalar()` accepts value arguments as-is, so excess properties trigger an error.
52 changes: 52 additions & 0 deletions src/__tests__/api.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VariablesOf<typeof mutation>>().toEqualTypeOf<{
input: {
title: string;
description: string;
complete?: boolean | null | undefined;
};
}>();

const vars = (input: VariablesOf<typeof mutation>) => input;

// @ts-expect-error
vars({ excess: true, input: { title: 'title', description: 'description' } });
});
});

describe('graphql() with custom scalars', () => {
Expand Down Expand Up @@ -310,6 +333,16 @@ describe('graphql.scalar()', () => {
expectTypeOf<actual>().toEqualTypeOf<expected>();
});

it('should return the type of a given input object', () => {
type actual = ReturnType<typeof graphql.scalar<'TodoPayload'>>;

expectTypeOf<actual>().toEqualTypeOf<{
complete?: boolean | undefined | null;
title: string;
description: string;
}>();
});

it('should return the type of a given enum', () => {
type actual = ReturnType<typeof graphql.scalar<'String'>>;
expectTypeOf<actual>().toEqualTypeOf<string>();
Expand All @@ -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<typeof actual>().toEqualTypeOf<{
title: string;
description: string;
}>();

graphql.scalar('TodoPayload', {
title: 'title',
description: 'description',
// @ts-expect-error
excess: true,
});
});
});

describe('graphql.scalar() with custom scalars', () => {
Expand Down
36 changes: 29 additions & 7 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,39 @@ interface GraphQLTadaAPI<Schema extends SchemaLike, Config extends AbstractConfi
* const myEnumValue = graphql.scalar('myEnum', 'value');
* ```
*/
scalar<
const Name extends string,
const Value extends getScalarType<Name, Schema, null | undefined>,
>(
scalar<const Name extends string, Value>(
name: Name,
value: Value
value: getScalarType<Name, Schema> extends Value
? Value & getScalarType<Name, Schema>
: getScalarType<Name, Schema>
): Value;

scalar<const Name extends string, Value>(
name: Name,
value: getScalarType<Name, Schema> extends Value
? never extends Value
? never
: (Value & getScalarType<Name, Schema>) | null
: getScalarType<Name, Schema> | null
): Value | null;
scalar<const Name extends string, Value>(
name: Name,
value: getScalarType<Name, Schema> extends Value
? never extends Value
? never
: (Value & getScalarType<Name, Schema>) | undefined
: getScalarType<Name, Schema> | undefined
): Value | undefined;
scalar<const Name extends string, Value>(
name: Name,
value: getScalarType<Name, Schema> extends Value
? never extends Value
? never
: (Value & getScalarType<Name, Schema>) | null | undefined
: getScalarType<Name, Schema> | null | undefined
): Value | null | undefined;
scalar<const Name extends string>(
name: Name,
value?: getScalarType<Name, Schema>
value: getScalarType<Name, Schema> | undefined
): getScalarType<Name, Schema>;

/** Function to replace a GraphQL document with a persisted document.
Expand Down
24 changes: 5 additions & 19 deletions src/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ type unwrapTypeRec<TypeRef, Introspection extends SchemaLike, IsOptional> = Type
: null | Array<unwrapTypeRec<TypeRef['ofType'], Introspection, true>>
: TypeRef extends { name: any }
? IsOptional extends false
? _getScalarType<TypeRef['name'], Introspection>
: null | _getScalarType<TypeRef['name'], Introspection>
? getScalarType<TypeRef['name'], Introspection>
: null | getScalarType<TypeRef['name'], Introspection>
: unknown;

type unwrapTypeRefRec<Type, Introspection extends SchemaLike, IsOptional> = Type extends {
Expand All @@ -52,8 +52,8 @@ type unwrapTypeRefRec<Type, Introspection extends SchemaLike, IsOptional> = Type
: null | Array<unwrapTypeRefRec<Type['type'], Introspection, true>>
: Type extends { kind: Kind.NAMED_TYPE; name: any }
? IsOptional extends false
? _getScalarType<Type['name']['value'], Introspection>
: null | _getScalarType<Type['name']['value'], Introspection>
? getScalarType<Type['name']['value'], Introspection>
: null | getScalarType<Type['name']['value'], Introspection>
: unknown;

type _getVariablesRec<
Expand Down Expand Up @@ -90,7 +90,7 @@ type getVariablesType<
Introspection extends SchemaLike,
> = _getVariablesRec<Document['definitions'][0]['variableDefinitions'], Introspection>;

type _getScalarType<
type getScalarType<
TypeName,
Introspection extends SchemaLike,
> = TypeName extends keyof Introspection['types']
Expand All @@ -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<Introspection['types'][TypeName]['inputFields'], Introspection> | 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 };

0 comments on commit 66b585c

Please sign in to comment.