diff --git a/.changeset/slow-planets-design.md b/.changeset/slow-planets-design.md new file mode 100644 index 00000000000..72cc2f0e259 --- /dev/null +++ b/.changeset/slow-planets-design.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +useQuery: `variables/options` should be required if `TVariables` is not empty/purely optional/default diff --git a/src/react/components/Query.tsx b/src/react/components/Query.tsx index 428207784c7..40c69605c79 100644 --- a/src/react/components/Query.tsx +++ b/src/react/components/Query.tsx @@ -18,7 +18,7 @@ export function Query< props: QueryComponentOptions ): ReactTypes.JSX.Element | null { const { children, query, ...options } = props; - const result = useQuery(query, options); + const result = useQuery(query, options); return result ? children(result as any) : null; } diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 938781a9890..fce91f28a56 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -8565,4 +8565,116 @@ describe.skip("Type Tests", () => { // @ts-expect-error variables?.nonExistingVariable; }); + + describe("optional/required options/variables scenarios", () => { + test("untyped document node, variables are optional and can be anything", () => { + const query = {} as DocumentNode; + useQuery(query); + useQuery(query, {}); + useQuery(query, { variables: {} }); + useQuery(query, { variables: { opt: "opt" } }); + useQuery(query, { variables: { req: "req" } }); + }); + test("typed document node with unspecified TVariables, variables are optional and can be anything", () => { + const query = {} as TypedDocumentNode<{ result: string }>; + useQuery(query); + useQuery(query, {}); + useQuery(query, { variables: {} }); + useQuery(query, { variables: { opt: "opt" } }); + useQuery(query, { variables: { req: "req" } }); + }); + test("empty variables are optional", () => { + const query = {} as TypedDocumentNode< + { result: string }, + Record + >; + useQuery(query); + useQuery(query, {}); + useQuery(query, { variables: {} }); + useQuery(query, { + variables: { + // @ts-expect-error on unknown variable + foo: "bar", + }, + }); + }); + test("all-optional variables are optional", () => { + const query = {} as TypedDocumentNode< + { result: string }, + { opt?: string } + >; + useQuery(query); + useQuery(query, {}); + useQuery(query, { variables: {} }); + useQuery(query, { variables: { opt: "opt" } }); + useQuery(query, { + variables: { + // @ts-expect-error on unknown variable + foo: "bar", + }, + }); + useQuery(query, { + variables: { + opt: "opt", + // @ts-expect-error on unknown variable + foo: "bar", + }, + }); + }); + test("non-optional variables are required", () => { + const query = {} as TypedDocumentNode< + { result: string }, + { req: string } + >; + // @ts-expect-error on missing options + useQuery(query); + useQuery( + query, + // @ts-expect-error on missing variables + {} + ); + useQuery(query, { + // @ts-expect-error on empty variables + variables: {}, + }); + useQuery(query, { + variables: { + // @ts-expect-error on unknown variable + foo: "bar", + }, + }); + useQuery(query, { variables: { req: "req" } }); + useQuery(query, { + variables: { + req: "req", + // @ts-expect-error on unknown variable + foo: "bar", + }, + }); + }); + test("mixed variables are required", () => { + const query = {} as TypedDocumentNode< + { result: string }, + { req: string; opt?: string } + >; + // @ts-expect-error on missing options + useQuery(query); + // @ts-expect-error on missing variables + useQuery(query, {}); + + useQuery(query, { + // @ts-expect-error on empty variables + variables: {}, + }); + + useQuery(query, { + // @ts-expect-error on missing required variable + variables: { + opt: "opt", + }, + }); + useQuery(query, { variables: { req: "req" } }); + useQuery(query, { variables: { req: "req", opt: "opt" } }); + }); + }); }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index b83f8558888..57fc4357856 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -41,6 +41,37 @@ const { prototype: { hasOwnProperty }, } = Object; +interface QueryHookOptionsWithVariables< + TData, + TVariables extends OperationVariables, +> extends Omit, "variables"> { + variables: TVariables; +} + +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]-?: T[K]; +}; + +type HasRequiredVariables = + {} extends OnlyRequiredProperties ? FalseCase : TrueCase; + +export function useQuery< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + ...[options]: HasRequiredVariables< + TVariables, + [ + optionsWithVariables: QueryHookOptionsWithVariables< + NoInfer, + NoInfer + >, + ], + [options?: QueryHookOptions, NoInfer>] + > +): QueryResult; + export function useQuery< TData = any, TVariables extends OperationVariables = OperationVariables,