Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/fetching/mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ order: 2

Now that we've learned how to [fetch data](./queries.md), the next step is to learn how to update that data with mutations.

> The examples below use the **classic** signature style (the default), where `<TData, TVariables>` generics are passed explicitly. If you'd rather have them inferred from a `TypedDocumentNode`, see [Modern signatures](../modern-signatures.md).

## Executing a Mutation

Let's define our GraphQL Mutation document.
Expand Down
2 changes: 2 additions & 0 deletions docs/fetching/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ For the purpose of this guide, we will be using GTS (Glimmer TypeScript) format
with inline templates instead of separated template files. This approach uses
the modern `<template>` syntax available in Ember.js.

> The examples below use the **classic** signature style (the default), where `<TData, TVariables>` generics are passed explicitly. If you'd rather have them inferred from a `TypedDocumentNode`, see [Modern signatures](../modern-signatures.md).

## Executing a Query

Let's first define our GraphQL Query document.
Expand Down
2 changes: 2 additions & 0 deletions docs/fetching/subscriptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Subscriptions enable you to fetch data for long-lasting operations that can chan

Subscriptions help notify your client in real-time about changes to back-end data, such as adding new objects, updated fields, and so on.

> The examples below pass `<TData, TVariables>` generics explicitly. If you'd rather have them inferred from a `TypedDocumentNode`, see [Modern signatures](../modern-signatures.md). Unlike `useQuery`/`useMutation`, `useSubscription` does not have a Classic/Modern split — the inference improvement applies to all callers.

## Client Setup

As subscriptions usually maintain a persistent connection, they shouldn't use
Expand Down
123 changes: 123 additions & 0 deletions docs/modern-signatures.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Modern signatures

Apollo Client 4.2 introduced two parallel hook signature styles, "classic" and "modern", switched globally via a single TypeScript declaration. Glimmer Apollo mirrors Apollo's pattern so you can opt into the same inferred-from-`TypedDocumentNode` ergonomics across `useQuery`, `useMutation`, and `useSubscription`.

Classic is the default, so existing call sites compile unchanged unless you opt into modern signatures; opt-in migrations are covered below.

> See [Apollo's 4.2 release notes](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md) for the upstream announcement.

## Opting in

Add a single declaration to a global types file (commonly `types/global.d.ts`):

```ts:types/global.d.ts
import '@apollo/client';

declare module '@apollo/client' {
export interface TypeOverrides {
signatureStyle: 'modern';
}
}
```

That's it. After this augmentation, `useQuery`/`useMutation`/`useSubscription` infer `TData` and `TVariables` from a `TypedDocumentNode` and you can drop the explicit `<TData, TVariables>` generics at call sites.

If you use a GraphQL codegen tool (such as [GraphQL Code Generator](https://the-guild.dev/graphql/codegen)) configured to emit `TypedDocumentNode`, your `.graphql`/`.gql` documents already carry the type information needed for inference.

## Before / after

The runtime is identical — only the declared types change.

### `useQuery`

```ts
// Classic (default) — generics are mandatory, document type is duplicated
notes = useQuery<GetNotesQuery, GetNotesQueryVariables>(this, () => [
GET_NOTES,
{ variables: { isArchived: this.isArchived } }
]);

// Modern — TData and TVariables inferred from GET_NOTES
notes = useQuery(this, () => [
GET_NOTES,
{ variables: { isArchived: this.isArchived } }
]);
```

### `useMutation`

```ts
// Classic
createNote = useMutation<CreateNoteMutation, CreateNoteMutationVariables>(
this,
() => [CREATE_NOTE]
);
await this.createNote.mutate({ title: 'Hi', description: '...' });

// Modern
createNote = useMutation(this, () => [CREATE_NOTE]);
await this.createNote.mutate({ title: 'Hi', description: '...' });
```

### `useSubscription`

`useSubscription` does not have a Classic/Modern split (matching Apollo Client 4.2's own subscription hook), but it does accept a `TypedDocumentNode` and infer types from it without explicit generics.

```ts
// Classic, explicit generics — still works
latestMessage = useSubscription<LatestMessageSubscription, LatestMessageSubscriptionVariables>(
this,
() => [LATEST_MESSAGE, { variables: { channel: 'general' } }]
);

// Inferred from the document
latestMessage = useSubscription(this, () => [
LATEST_MESSAGE,
{ variables: { channel: 'general' } }
]);
```

## What you gain

Apart from less typing, opting in catches a small set of mistakes the classic shape silently accepts when generics are inferred-as-default. The wins are most visible at call sites that already use `TypedDocumentNode`:

- **`variables` at the options site is checked against the document.** A schema rename or wrong-typed value becomes a compile error rather than a silent runtime miss.

```ts
// GetNotes's variables are { isArchived?: boolean | null }
notes = useQuery(this, () => [
GET_NOTES,
// @ts-expect-error — `archive` is not a declared variable
{ variables: { archive: true } }
]);
```

- **`.mutate(vars)` checks `vars` against the document's `TVariables`.** Under classic this check requires explicit generics; under modern you get it for free at inferred call sites.

```ts
createNote = useMutation(this, () => [CREATE_NOTE]);
// @ts-expect-error — missing required `description`
await this.createNote.mutate({ title: 'Hi' });
```

## Migration notes

For consumers that opt in **and** use a single-generic call style with narrowly-typed documents, modern is stricter than classic about a few patterns. These are deliberate — the point of opting in is catching them — but listed here as a heads-up:

- **Single-generic `useQuery<MyQuery>` no longer compiles under modern.** Drop the generic and let `TypedDocumentNode` infer instead. Under modern, the unspecified `TVariables` defaults to `OperationVariables`, and a `TypedDocumentNode<MyQuery, MyQueryVariables>` does not satisfy `TypedDocumentNode<MyQuery, OperationVariables>` (`TVariables` is contravariant).

```ts
// Classic — compiles
notes = useQuery<GetNotesQuery>(this, () => [GET_NOTES]);

// Modern — does not compile; drop the generic
notes = useQuery(this, () => [GET_NOTES]);
```

- **Plain `DocumentNode` (no `TypedDocumentNode`) still works under modern**, just with `unknown` for `TData`/`TVariables`. No migration needed for documents that aren't typed.

## Notes

- This wrapper's `Signatures.Modern` keeps `<TData = unknown, TVariables = OperationVariables>` defaults, so passing explicit generics at a modern call site is allowed (e.g. for gradual migration). Apollo Client's own modern signatures block explicit generics via a phantom inference-only type parameter ([Apollo 4.2 CHANGELOG](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md), under PR #13132). The wrapper deliberately diverges to ease incremental adoption.
- `Signatures.Classic`/`Signatures.Modern` and their `Evaluated` switch use `SignatureStyle` from `@apollo/client/utilities/internal`. That subpath is listed in Apollo's `package.json` exports, though its `internal` naming and lack of documented stability commitments mean it should be treated as advanced surface.
- Requires `@apollo/client@^4.2.0`.
6 changes: 3 additions & 3 deletions glimmer-apollo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"decorator-transforms": "^2.2.2"
},
"devDependencies": {
"@apollo/client": "^4.0.0",
"@apollo/client": "^4.2.0",
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.28.5",
"@babel/plugin-transform-typescript": "^7.28.5",
Expand Down Expand Up @@ -81,8 +81,8 @@
"typescript-eslint": "^8.50.0"
},
"peerDependencies": {
"@apollo/client": "^4.0.0",
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0",
"@apollo/client": "^4.2.0",
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0",
"rxjs": "^7.0.0"
},
"ember": {
Expand Down
9 changes: 7 additions & 2 deletions glimmer-apollo/src/-private/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
MutateResult,
OperationVariables,
MaybeMasked,
TypedDocumentNode,
} from '@apollo/client';
import type { TemplateArgs } from './types';

Expand All @@ -23,7 +24,8 @@ type Maybe<T> = T | undefined | null;
export type MutationOptions<
TData,
TVariables extends OperationVariables,
> = Omit<ApolloMutationOptions<TData, TVariables>, 'mutation'> & {
> = Omit<ApolloMutationOptions<TData, TVariables>, 'mutation' | 'variables'> & {
variables?: TVariables;
clientId?: string;
onComplete?: (data: Maybe<MaybeMasked<TData>>) => void;
onError?: (error: ErrorLike) => void;
Expand All @@ -32,7 +34,10 @@ export type MutationOptions<
export type MutationPositionalArgs<
TData,
TVariables extends OperationVariables = OperationVariables,
> = [DocumentNode, MutationOptions<TData, TVariables>?];
> = [
DocumentNode | TypedDocumentNode<TData, TVariables>,
MutationOptions<TData, TVariables>?,
];

export class MutationResource<
TData,
Expand Down
6 changes: 5 additions & 1 deletion glimmer-apollo/src/-private/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
MaybeMasked,
OperationVariables,
ObservableQuery,
TypedDocumentNode,
} from '@apollo/client';
import type { Subscription } from 'rxjs';
import type { TemplateArgs } from './types';
Expand All @@ -36,7 +37,10 @@ export type QueryOptions<TData, TVariables extends OperationVariables> = Omit<
export type QueryPositionalArgs<
TData,
TVariables extends OperationVariables = OperationVariables,
> = [DocumentNode, QueryOptions<TData, TVariables>?];
> = [
DocumentNode | TypedDocumentNode<TData, TVariables>,
QueryOptions<TData, TVariables>?,
];

export class QueryResource<
TData,
Expand Down
6 changes: 5 additions & 1 deletion glimmer-apollo/src/-private/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
MaybeMasked,
OperationVariables,
SubscriptionOptions as ApolloSubscriptionOptions,
TypedDocumentNode,
} from '@apollo/client';
import type { Subscription } from 'rxjs';
import { equal } from '@wry/equality';
Expand All @@ -32,7 +33,10 @@ export type SubscriptionOptions<
export type SubscriptionPositionalArgs<
TData,
TVariables extends OperationVariables = OperationVariables,
> = [DocumentNode, SubscriptionOptions<TData, TVariables>?];
> = [
DocumentNode | TypedDocumentNode<TData, TVariables>,
SubscriptionOptions<TData, TVariables>?,
];

export class SubscriptionResource<
TData,
Expand Down
88 changes: 83 additions & 5 deletions glimmer-apollo/src/-private/usables.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
import { useResource } from './use-resource.ts';
import { type MutationPositionalArgs, MutationResource } from './mutation.ts';
import { type QueryPositionalArgs, QueryResource } from './query.ts';
import {
type MutationOptions,
type MutationPositionalArgs,
MutationResource,
} from './mutation.ts';
import {
type QueryOptions,
type QueryPositionalArgs,
QueryResource,
} from './query.ts';
import {
type SubscriptionPositionalArgs,
SubscriptionResource,
} from './subscription.ts';
import type { OperationVariables } from '@apollo/client';
import type { OperationVariables, TypedDocumentNode } from '@apollo/client';
import type {
NoInfer,
SignatureStyle,
} from '@apollo/client/utilities/internal';

/* eslint-disable @typescript-eslint/no-namespace, @typescript-eslint/no-empty-object-type --
Namespaces and the empty-extends interface mirror Apollo Client 4.2's own
`useQuery.Signatures.{Classic,Modern}` pattern. Declaration merging via
namespaces is the only way to expose the typed members alongside the
runtime const. */

export namespace useQuery {
export namespace Signatures {
export interface Classic {
<
TData = unknown,
TVariables extends OperationVariables = OperationVariables,
>(
parentDestroyable: object,
args: () => QueryPositionalArgs<TData, TVariables>,
): QueryResource<TData, TVariables>;
}
export interface Modern {
<
TData = unknown,
TVariables extends OperationVariables = OperationVariables,
>(
parentDestroyable: object,
args: () => [
TypedDocumentNode<TData, TVariables>,
QueryOptions<TData, NoInfer<TVariables>>?,
],
): QueryResource<TData, TVariables>;
}
export type Evaluated = SignatureStyle extends 'classic' ? Classic : Modern;
}
export interface Signature extends Signatures.Evaluated {}
}

export function useQuery<
function useQueryImpl<
TData = unknown,
TVariables extends OperationVariables = OperationVariables,
>(
Expand All @@ -19,8 +65,37 @@ export function useQuery<
QueryResource<TData, TVariables>
>(parentDestroyable, QueryResource, args);
}
export const useQuery: useQuery.Signature = useQueryImpl;

export namespace useMutation {
export namespace Signatures {
export interface Classic {
<
TData = unknown,
TVariables extends OperationVariables = OperationVariables,
>(
parentDestroyable: object,
args: () => MutationPositionalArgs<TData, TVariables>,
): MutationResource<TData, TVariables>;
}
export interface Modern {
<
TData = unknown,
TVariables extends OperationVariables = OperationVariables,
>(
parentDestroyable: object,
args: () => [
TypedDocumentNode<TData, TVariables>,
MutationOptions<TData, NoInfer<TVariables>>?,
],
): MutationResource<TData, TVariables>;
}
export type Evaluated = SignatureStyle extends 'classic' ? Classic : Modern;
}
export interface Signature extends Signatures.Evaluated {}
}

export function useMutation<
function useMutationImpl<
TData = unknown,
TVariables extends OperationVariables = OperationVariables,
>(
Expand All @@ -32,6 +107,9 @@ export function useMutation<
MutationResource<TData, TVariables>
>(parentDestroyable, MutationResource, args);
}
export const useMutation: useMutation.Signature = useMutationImpl;

/* eslint-enable @typescript-eslint/no-namespace, @typescript-eslint/no-empty-object-type */

export function useSubscription<
TData = unknown,
Expand Down
Loading
Loading