Skip to content

Commit 2d5c236

Browse files
committed
WIP updates for TOptions
1 parent 7ab8630 commit 2d5c236

1 file changed

Lines changed: 103 additions & 23 deletions

File tree

src/react/hooks/useQuery.ts

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,73 @@ export declare namespace useQuery {
198198
skip: false;
199199
}
200200

201+
/**
202+
* The constraint for `TOptions` in the `Modern` signatures.
203+
*
204+
* `variables` is deliberately only `unknown` here - see the `Modern`
205+
* signatures for why the real check cannot live in the constraint. It still
206+
* has to be listed: `Base.Options` is a weak type (every property is
207+
* optional), so `{ variables: ... }` on its own would share no property with
208+
* it and be rejected.
209+
*
210+
* The mapped type reports options that are not part of `useQuery.Options`.
211+
* `TOptions` cannot reject them on its own because it is inferred from the
212+
* very object literal being checked, so every key the user writes becomes part
213+
* of it. Two details matter here:
214+
*
215+
* - The properties are required rather than optional. An optional `never`
216+
* would give a misspelled option a property in common with this weak type,
217+
* which is enough to satisfy the weak type check and let it through.
218+
* - `SkipToken` is excluded from `TOptions` first. A conditionally skipped
219+
* query infers `TOptions` as a union of `SkipToken` and the options object,
220+
* and `keyof` of that union is `never`, which would silently make this a
221+
* no-op.
222+
*/
223+
export type ConstraintFor<
224+
TOptions,
225+
TData,
226+
TVariables extends OperationVariables,
227+
> = Base.Options<TData, NoInfer<TVariables>> & {
228+
variables?: unknown;
229+
} & ([
230+
Exclude<
231+
keyof Exclude<TOptions, SkipToken>,
232+
keyof Options<TData, TVariables>
233+
>,
234+
] extends [never] ?
235+
unknown
236+
: never);
237+
238+
/**
239+
* The `variables` option as it is required at the parameter position of the
240+
* `Modern` signatures.
241+
*
242+
* `NoInfer` keeps this from becoming an inference site for `TVariables`. The
243+
* mapped type reports variables that are not part of `TVariables`, for the
244+
* same reason `ConstraintFor` has to check the option names.
245+
*/
246+
export type OptionsFor<
247+
TOptions,
248+
TData,
249+
TVariables extends OperationVariables,
250+
> = Options<TData, NoInfer<TVariables>> & {
251+
variables?: {
252+
// variables that are not part of `TVariables`
253+
[K in Exclude<
254+
keyof TOptions["variables" & keyof TOptions],
255+
keyof TVariables
256+
>]?: never;
257+
};
258+
};
259+
201260
export type ResultForOptions<
202261
TData,
203262
TVariables extends OperationVariables,
204263
TOptions extends
205264
| Record<string, never> // no options
206-
| Options<TData, TVariables>
265+
// `Base.Options` instead of `Options` because the `variables` option is
266+
// not part of `TOptions` - see the `Modern` signatures for details.
267+
| Base.Options<TData, TVariables>
207268
| SkipToken,
208269
> = LazyType<
209270
Result<
@@ -519,6 +580,28 @@ export declare namespace useQuery {
519580
}
520581

521582
/** {@inheritDoc @apollo/client/react!useQuery.DocumentationTypes.useQuery:call(1)} */
583+
// The exact `variables` check lives at the parameter position
584+
// (`ConstraintFor` only says `variables?: unknown`), because a constraint
585+
// cannot do it.
586+
//
587+
// TypeScript widens fresh literals when inferring an object literal into a
588+
// naked type parameter, so `useQuery(query, { variables: { type: "main" } })`
589+
// infers the candidate `{ variables: { type: string } }`. A constraint that
590+
// required the exact `TVariables` (`{ type: "main" }`) would reject that
591+
// candidate, and TypeScript then silently substitutes the constraint for
592+
// `TOptions` - discarding every other inferred option along with it. That is
593+
// what made `returnPartialData: false` come back as `boolean` and put
594+
// `"partial"` into `dataState`.
595+
//
596+
// Widening also means the constraint could not do the check even if it did
597+
// not collapse: once `{ type: "nope" }` has widened to `{ type: string }`
598+
// there is nothing left to distinguish it from a valid value. Only the
599+
// parameter position still sees the literal.
600+
//
601+
// `NoInfer` on `TVariables` there is required. Without it,
602+
// `VariablesOption<TVariables>` becomes an inference site and the widened
603+
// `{ type: string }` candidate wins over the one from the document, which
604+
// stops invalid variable values from being reported at all.
522605
export interface Modern {
523606
/** {@inheritDoc @apollo/client/react!useQuery.DocumentationTypes.useQuery:call(1)} */
524607
<
@@ -538,24 +621,20 @@ export declare namespace useQuery {
538621
TData,
539622
TVariables extends OperationVariables,
540623
// 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-
>,
624+
TOptions extends useQuery.ConstraintFor<TOptions, TData, TVariables>,
550625
>(
551626
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
552627
...[options]: // we generally do not allow for a `TVariables` of `never`
553628
// TODO: check if we need a similar check in other hooks
554629
[TVariables] extends [never] ? [options: never]
555630
: // variables optional
556-
{} extends TVariables ? [options?: TOptions]
631+
{} extends TVariables ?
632+
[
633+
options?: TOptions &
634+
useQuery.OptionsFor<TOptions, TData, TVariables>,
635+
]
557636
: // variables required
558-
[options: TOptions]
637+
[options: TOptions & useQuery.OptionsFor<TOptions, TData, TVariables>]
559638
): useQuery.ResultForOptions<TData, TVariables, TOptions>;
560639

561640
/** {@inheritDoc @apollo/client/react!useQuery.DocumentationTypes.useQuery:call(1)} */
@@ -574,24 +653,25 @@ export declare namespace useQuery {
574653
TData,
575654
TVariables extends OperationVariables,
576655
// 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-
>,
656+
TOptions extends useQuery.ConstraintFor<TOptions, TData, TVariables>,
586657
>(
587658
query: DocumentNode | TypedDocumentNode<TData, TVariables>,
588659
...[options]: // we generally do not allow for a `TVariables` of `never`
589660
// TODO: check if we need a similar check in other hooks
590661
[TVariables] extends [never] ? [options: never]
591662
: // variables optional
592-
{} extends TVariables ? [options?: TOptions | SkipToken]
663+
{} extends TVariables ?
664+
[
665+
options?:
666+
| (TOptions & useQuery.OptionsFor<TOptions, TData, TVariables>)
667+
| SkipToken,
668+
]
593669
: // variables required
594-
[options: TOptions | SkipToken]
670+
[
671+
options:
672+
| (TOptions & useQuery.OptionsFor<TOptions, TData, TVariables>)
673+
| SkipToken,
674+
]
595675
): useQuery.ResultForOptions<TData, TVariables, TOptions | SkipToken>;
596676

597677
ssrDisabledResult: ObservableQuery.Result<any>;

0 commit comments

Comments
 (0)