Skip to content

Commit 0f497d9

Browse files
committed
Fix literal vars causing issues
1 parent 7ab8630 commit 0f497d9

2 files changed

Lines changed: 106 additions & 28 deletions

File tree

integration-tests/type-tests/signatures/modern/useQuery.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,11 +421,8 @@ test("rejects unknown options", () => {
421421
});
422422

423423
let skip!: boolean;
424-
useQuery(
425-
noVariables,
426-
// @ts-expect-error unknown option
427-
skip ? skipToken : { returnPartialDta: false }
428-
);
424+
// @ts-expect-error unknown option
425+
useQuery(noVariables, skip ? skipToken : { returnPartialDta: false });
429426
useQuery(
430427
literalVariables,
431428
// @ts-expect-error unknown option

src/react/hooks/useQuery.ts

Lines changed: 104 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type {
4040
LazyType,
4141
NoInfer,
4242
OptionWithFallback,
43+
Prettify,
4344
SignatureStyle,
4445
VariablesOption,
4546
} from "@apollo/client/utilities/internal";
@@ -198,12 +199,69 @@ export declare namespace useQuery {
198199
skip: false;
199200
}
200201

202+
/**
203+
* The options as they are checked at the parameter position of the `Modern`
204+
* signatures, intersected with the inferred `TOptions`.
205+
*
206+
* `NoInfer` keeps this from becoming an inference site for `TVariables`.
207+
*
208+
* The two mapped types report variables that are not part of `TVariables` and
209+
* options that are not part of `useQuery.Options`. `TOptions` cannot reject
210+
* either on its own, because it is inferred from the very object literal being
211+
* checked - every key the user writes becomes part of it. Two details matter
212+
* for the option names:
213+
*
214+
* - `SkipToken` is excluded from `TOptions` first. A conditionally skipped
215+
* query infers `TOptions` as a union of `SkipToken` and the options object,
216+
* and `keyof` of that union is `never`, which would silently make the check
217+
* a no-op.
218+
* - The conditional is what keeps the passing case at `unknown` instead of an
219+
* empty mapped type. An empty object type is not a weak type, and an
220+
* intersection is only weak if every member is, so intersecting one in would
221+
* turn the weak type check off for the whole parameter. That check is not
222+
* only about rejecting options objects without a single known option - it
223+
* also affects inference. Without it, an options argument of the type
224+
* `SkipToken | { returnPartialData: false }` infers a `TVariables` that has
225+
* lost the optionality of its properties.
226+
*/
227+
export type OptionsFor<
228+
TOptions,
229+
TData,
230+
TVariables extends OperationVariables,
231+
> = Options<TData, NoInfer<TVariables>> & {
232+
variables?: Prettify<
233+
TVariables & {
234+
// variables that are not part of `TVariables`
235+
[K in keyof TOptions["variables" & keyof TOptions]]: K extends (
236+
keyof TVariables
237+
) ?
238+
TVariables[K]
239+
: never;
240+
}
241+
>;
242+
} & ([
243+
Exclude<
244+
keyof Exclude<TOptions, SkipToken>,
245+
keyof Options<TData, TVariables>
246+
>,
247+
] extends [never] ?
248+
unknown
249+
: {
250+
// options that are not part of `useQuery.Options`
251+
[K in Exclude<
252+
keyof Exclude<TOptions, SkipToken>,
253+
keyof Options<TData, TVariables>
254+
>]: never;
255+
});
256+
201257
export type ResultForOptions<
202258
TData,
203259
TVariables extends OperationVariables,
204260
TOptions extends
205261
| Record<string, never> // no options
206-
| Options<TData, TVariables>
262+
// `Base.Options` instead of `Options` because the `variables` option is
263+
// not part of `TOptions` - see the `Modern` signatures for details.
264+
| Base.Options<TData, TVariables>
207265
| SkipToken,
208266
> = LazyType<
209267
Result<
@@ -519,6 +577,28 @@ export declare namespace useQuery {
519577
}
520578

521579
/** {@inheritDoc @apollo/client/react!useQuery.DocumentationTypes.useQuery:call(1)} */
580+
// Every check on the options lives at the parameter position, in
581+
// `OptionsFor`. The constraint only describes the shape of an options
582+
// object, because a constraint cannot do the checking.
583+
//
584+
// TypeScript widens fresh literals when inferring an object literal into a
585+
// naked type parameter, so `useQuery(query, { variables: { type: "main" } })`
586+
// infers the candidate `{ variables: { type: string } }`. A constraint that
587+
// required the exact `TVariables` (`{ type: "main" }`) would reject that
588+
// candidate, and TypeScript then silently substitutes the constraint for
589+
// `TOptions` - discarding every other inferred option along with it. That is
590+
// what made `returnPartialData: false` come back as `boolean` and put
591+
// `"partial"` into `dataState`.
592+
//
593+
// Widening also means the constraint could not do the check even if it did
594+
// not collapse: once `{ type: "nope" }` has widened to `{ type: string }`
595+
// there is nothing left to distinguish it from a valid value. Only the
596+
// parameter position still sees the literal.
597+
//
598+
// `NoInfer` on `TVariables` there is required. Without it,
599+
// `VariablesOption<TVariables>` becomes an inference site and the widened
600+
// `{ type: string }` candidate wins over the one from the document, which
601+
// stops invalid variable values from being reported at all.
522602
export interface Modern {
523603
/** {@inheritDoc @apollo/client/react!useQuery.DocumentationTypes.useQuery:call(1)} */
524604
<
@@ -538,24 +618,22 @@ export declare namespace useQuery {
538618
TData,
539619
TVariables extends OperationVariables,
540620
// this overload should never be manually defined, it should always be inferred
541-
TOptions extends useQuery.Options<TData, NoInfer<TVariables>> &
542-
VariablesOption<
543-
TVariables & {
544-
[K in Exclude<
545-
keyof TOptions["variables"],
546-
keyof TVariables
547-
>]?: never;
548-
}
549-
>,
621+
TOptions extends Base.Options<TData, NoInfer<TVariables>> & {
622+
variables?: unknown;
623+
},
550624
>(
551625
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
552626
...[options]: // we generally do not allow for a `TVariables` of `never`
553627
// TODO: check if we need a similar check in other hooks
554628
[TVariables] extends [never] ? [options: never]
555629
: // variables optional
556-
{} extends TVariables ? [options?: TOptions]
630+
{} extends TVariables ?
631+
[
632+
options?: TOptions &
633+
useQuery.OptionsFor<TOptions, TData, TVariables>,
634+
]
557635
: // variables required
558-
[options: TOptions]
636+
[options: TOptions & useQuery.OptionsFor<TOptions, TData, TVariables>]
559637
): useQuery.ResultForOptions<TData, TVariables, TOptions>;
560638

561639
/** {@inheritDoc @apollo/client/react!useQuery.DocumentationTypes.useQuery:call(1)} */
@@ -574,24 +652,27 @@ export declare namespace useQuery {
574652
TData,
575653
TVariables extends OperationVariables,
576654
// this overload should never be manually defined, it should always be inferred
577-
TOptions extends useQuery.Options<TData, NoInfer<TVariables>> &
578-
VariablesOption<
579-
TVariables & {
580-
[K in Exclude<
581-
keyof TOptions["variables"],
582-
keyof TVariables
583-
>]?: never;
584-
}
585-
>,
655+
TOptions extends Base.Options<TData, NoInfer<TVariables>> & {
656+
variables?: unknown;
657+
},
586658
>(
587659
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
588660
...[options]: // we generally do not allow for a `TVariables` of `never`
589661
// TODO: check if we need a similar check in other hooks
590662
[TVariables] extends [never] ? [options: never]
591663
: // variables optional
592-
{} extends TVariables ? [options?: TOptions | SkipToken]
664+
{} extends TVariables ?
665+
[
666+
options?:
667+
| (TOptions & useQuery.OptionsFor<TOptions, TData, TVariables>)
668+
| SkipToken,
669+
]
593670
: // variables required
594-
[options: TOptions | SkipToken]
671+
[
672+
options:
673+
| (TOptions & useQuery.OptionsFor<TOptions, TData, TVariables>)
674+
| SkipToken,
675+
]
595676
): useQuery.ResultForOptions<TData, TVariables, TOptions | SkipToken>;
596677

597678
ssrDisabledResult: ObservableQuery.Result<any>;

0 commit comments

Comments
 (0)