Skip to content

Commit 11be77c

Browse files
authored
Add datatag to ConnectQueryKey so queryClient can infer type info (#532)
By adding DataTag annotations to the ConnectQueryKey, QueryClient methods can now infer the return type when calling things like `getQueryData` or `invalidateQueries`. This is at least a partial alternative to #468 which provides better inference abilities while keeping our api surface smaller. There may still be value down the line with very well defined query client methods but I suspect this change covers 90% of the current use cases. --------- Signed-off-by: Paul Sachs <psachs@buf.build>
1 parent 8e26eb6 commit 11be77c

File tree

12 files changed

+227
-79
lines changed

12 files changed

+227
-79
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
node_modules
44
/packages/*/dist
55
/packages/*/coverage
6+
tsconfig.vitest-temp.json

README.md

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -260,19 +260,17 @@ Any additional `options` you pass to `useMutation` will be merged with the optio
260260

261261
### `createConnectQueryKey`
262262

263-
```ts
264-
function createConnectQueryKey<Desc extends DescMethod | DescService>(
265-
params: KeyParams<Desc>,
266-
): ConnectQueryKey;
267-
```
268-
269-
This function is used under the hood of `useQuery` and other hooks to compute a [`queryKey`](https://tanstack.com/query/v4/docs/react/guides/query-keys) for TanStack Query. You can use it to create (partial) keys yourself to filter queries.
263+
This function is used under the hood of `useQuery` and other hooks to compute a [`queryKey`](https://tanstack.com/query/v4/docs/react/guides/query-keys) for TanStack Query. You can use it to create keys yourself to filter queries.
270264

271265
`useQuery` creates a query key with the following parameters:
272266

273267
1. The qualified name of the RPC.
274268
2. The transport being used.
275269
3. The request message.
270+
4. The cardinality of the RPC (either "finite" or "infinite").
271+
5. Adds a DataTag which brands the key with the associated data type of the response.
272+
273+
The DataTag type allows @tanstack/react-query functions to properly infer the type of the data returned by the query. This is useful for things like `QueryClient.setQueryData` and `QueryClient.getQueryData`.
276274

277275
To create the same key manually, you simply provide the same parameters:
278276

@@ -355,7 +353,7 @@ function callUnaryMethod<I extends DescMessage, O extends DescMessage>(
355353

356354
This API allows you to directly call the method using the provided transport. Use this if you need to manually call a method outside of the context of a React component, or need to call it where you can't use hooks.
357355

358-
### `createProtobufSafeUpdater`
356+
### `createProtobufSafeUpdater` (deprecated)
359357

360358
Creates a typesafe updater that can be used to update data in a query cache. Used in combination with a queryClient.
361359

@@ -387,6 +385,8 @@ queryClient.setQueryData(
387385

388386
```
389387

388+
** Note: This API is deprecated and will be removed in a future version. `ConnectQueryKey` now contains type information to make it safer to use `setQueryData` directly. **
389+
390390
### `createQueryOptions`
391391

392392
```ts
@@ -599,21 +599,15 @@ Connect-Query does require React, but the core (`createConnectQueryKey` and `cal
599599

600600
### How do I do Prefetching?
601601

602-
When you might not have access to React context, you can use the `create` series of functions and provide a transport directly. For example:
602+
When you might not have access to React context, you can use `createQueryOptions` and provide a transport directly. For example:
603603

604604
```ts
605605
import { say } from "./gen/eliza-ElizaService_connectquery";
606606

607607
function prefetch() {
608-
return queryClient.prefetchQuery({
609-
queryKey: createConnectQueryKey({
610-
schema: say,
611-
transport: myTransport,
612-
input: { sentence: "Hello" },
613-
cardinality: "finite",
614-
}),
615-
queryFn: () => callUnaryMethod(myTransport, say, { sentence: "Hello" }),
616-
});
608+
return queryClient.prefetchQuery(
609+
createQueryOptions(say, { sentence: "Hello" }, { transport: myTransport }),
610+
);
617611
}
618612
```
619613

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/connect-query-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"peerDependencies": {
4545
"@bufbuild/protobuf": "2.x",
4646
"@connectrpc/connect": "^2.0.1",
47-
"@tanstack/query-core": "5.x"
47+
"@tanstack/query-core": ">=5.62.0"
4848
},
4949
"files": [
5050
"dist/**"

packages/connect-query-core/src/connect-query-key.test.ts

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@
1414

1515
import { create } from "@bufbuild/protobuf";
1616
import type { Transport } from "@connectrpc/connect";
17-
import { ElizaService, SayRequestSchema } from "test-utils/gen/eliza_pb.js";
17+
import {
18+
ElizaService,
19+
SayRequestSchema,
20+
SayResponseSchema,
21+
type SayResponse,
22+
} from "test-utils/gen/eliza_pb.js";
1823
import { ListRequestSchema, ListService } from "test-utils/gen/list_pb.js";
19-
import { describe, expect, it } from "vitest";
24+
import { describe, expect, expectTypeOf, it } from "vitest";
2025

2126
import { createConnectQueryKey } from "./connect-query-key.js";
2227
import { skipToken } from "./index.js";
2328
import { createMessageKey } from "./message-key.js";
2429
import { createTransportKey } from "./transport-key.js";
30+
import { type InfiniteData, QueryClient } from "@tanstack/query-core";
2531

2632
describe("createConnectQueryKey", () => {
2733
const fakeTransport: Transport = {
@@ -125,12 +131,96 @@ describe("createConnectQueryKey", () => {
125131

126132
it("cannot except invalid input", () => {
127133
createConnectQueryKey({
134+
// @ts-expect-error(2322) cannot create a key with invalid input
128135
schema: ElizaService.method.say,
129136
input: {
130-
// @ts-expect-error(2322) cannot create a key with invalid input
131137
sentence: 1,
132138
},
133139
cardinality: undefined,
134140
});
135141
});
142+
143+
it("contains type hints to indicate the output type", () => {
144+
const sampleQueryClient = new QueryClient();
145+
const key = createConnectQueryKey({
146+
schema: ElizaService.method.say,
147+
input: create(SayRequestSchema, { sentence: "hi" }),
148+
cardinality: "finite",
149+
});
150+
const data = sampleQueryClient.getQueryData(key);
151+
152+
expectTypeOf(data).toEqualTypeOf<SayResponse | undefined>();
153+
});
154+
155+
it("supports typesafe data updaters", () => {
156+
const sampleQueryClient = new QueryClient();
157+
const key = createConnectQueryKey({
158+
schema: ElizaService.method.say,
159+
input: create(SayRequestSchema, { sentence: "hi" }),
160+
cardinality: "finite",
161+
});
162+
// @ts-expect-error(2345) this is a test to check if the type is correct
163+
sampleQueryClient.setQueryData(key, { sentence: 1 });
164+
// @ts-expect-error(2345) $typename is required
165+
sampleQueryClient.setQueryData(key, {
166+
sentence: "a proper value but missing $typename",
167+
});
168+
sampleQueryClient.setQueryData(
169+
key,
170+
create(SayResponseSchema, { sentence: "a proper value" }),
171+
);
172+
173+
sampleQueryClient.setQueryData(key, (prev) => {
174+
expectTypeOf(prev).toEqualTypeOf<SayResponse | undefined>();
175+
return create(SayResponseSchema, {
176+
sentence: "a proper value",
177+
});
178+
});
179+
});
180+
181+
describe("infinite queries", () => {
182+
it("contains type hints to indicate the output type", () => {
183+
const sampleQueryClient = new QueryClient();
184+
const key = createConnectQueryKey({
185+
schema: ElizaService.method.say,
186+
input: create(SayRequestSchema, { sentence: "hi" }),
187+
cardinality: "infinite",
188+
});
189+
const data = sampleQueryClient.getQueryData(key);
190+
191+
expectTypeOf(data).toEqualTypeOf<InfiniteData<SayResponse> | undefined>();
192+
});
193+
194+
it("supports typesafe data updaters", () => {
195+
const sampleQueryClient = new QueryClient();
196+
const key = createConnectQueryKey({
197+
schema: ElizaService.method.say,
198+
input: create(SayRequestSchema, { sentence: "hi" }),
199+
cardinality: "infinite",
200+
});
201+
sampleQueryClient.setQueryData(key, {
202+
pages: [
203+
// @ts-expect-error(2345) make sure the shape is as expected
204+
{ sentence: 1 },
205+
],
206+
});
207+
sampleQueryClient.setQueryData(key, {
208+
// @ts-expect-error(2345) $typename is required
209+
pages: [{ sentence: "a proper value but missing $typename" }],
210+
});
211+
sampleQueryClient.setQueryData(key, {
212+
pageParams: [0],
213+
pages: [create(SayResponseSchema, { sentence: "a proper value" })],
214+
});
215+
sampleQueryClient.setQueryData(key, (prev) => {
216+
expectTypeOf(prev).toEqualTypeOf<
217+
InfiniteData<SayResponse> | undefined
218+
>();
219+
return {
220+
pageParams: [0],
221+
pages: [create(SayResponseSchema, { sentence: "a proper value" })],
222+
};
223+
});
224+
});
225+
});
136226
});

packages/connect-query-core/src/connect-query-key.ts

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,60 @@ import type {
1818
DescMethodUnary,
1919
DescService,
2020
MessageInitShape,
21+
MessageShape,
2122
} from "@bufbuild/protobuf";
22-
import type { Transport } from "@connectrpc/connect";
23-
import type { SkipToken } from "@tanstack/query-core";
23+
import type { ConnectError, Transport } from "@connectrpc/connect";
24+
import type { DataTag, InfiniteData, SkipToken } from "@tanstack/query-core";
2425

2526
import { createMessageKey } from "./message-key.js";
2627
import { createTransportKey } from "./transport-key.js";
2728

29+
type SharedConnectQueryOptions = {
30+
/**
31+
* A key for a Transport reference, created with createTransportKey().
32+
*/
33+
transport?: string;
34+
/**
35+
* The name of the service, e.g. connectrpc.eliza.v1.ElizaService
36+
*/
37+
serviceName: string;
38+
/**
39+
* The name of the method, e.g. Say.
40+
*/
41+
methodName?: string;
42+
/**
43+
* A key for the request message, created with createMessageKey(),
44+
* or "skipped".
45+
*/
46+
input?: Record<string, unknown> | "skipped";
47+
};
48+
49+
type InfiniteConnectQueryKey<OutputMessage extends DescMessage = DescMessage> =
50+
DataTag<
51+
[
52+
"connect-query",
53+
SharedConnectQueryOptions & {
54+
/** This data represents a infinite, paged result */
55+
cardinality: "infinite";
56+
},
57+
],
58+
InfiniteData<MessageShape<OutputMessage>>,
59+
ConnectError
60+
>;
61+
62+
type FiniteConnectQueryKey<OutputMessage extends DescMessage = DescMessage> =
63+
DataTag<
64+
[
65+
"connect-query",
66+
SharedConnectQueryOptions & {
67+
/** This data represents a finite result */
68+
cardinality: "finite";
69+
},
70+
],
71+
MessageShape<OutputMessage>,
72+
ConnectError
73+
>;
74+
2875
/**
2976
* TanStack Query manages query caching for you based on query keys. `QueryKey`s in TanStack Query are arrays with arbitrary JSON-serializable data - typically handwritten for each endpoint.
3077
*
@@ -44,35 +91,15 @@ import { createTransportKey } from "./transport-key.js";
4491
* }
4592
* ]
4693
*/
47-
export type ConnectQueryKey = [
48-
/**
49-
* To distinguish Connect query keys from other query keys, they always start with the string "connect-query".
50-
*/
51-
"connect-query",
52-
{
53-
/**
54-
* A key for a Transport reference, created with createTransportKey().
55-
*/
56-
transport?: string;
57-
/**
58-
* The name of the service, e.g. connectrpc.eliza.v1.ElizaService
59-
*/
60-
serviceName: string;
61-
/**
62-
* The name of the method, e.g. Say.
63-
*/
64-
methodName?: string;
65-
/**
66-
* A key for the request message, created with createMessageKey(),
67-
* or "skipped".
68-
*/
69-
input?: Record<string, unknown> | "skipped";
70-
/**
71-
* Whether this is an infinite query, or a regular one.
72-
*/
73-
cardinality?: "infinite" | "finite" | undefined;
74-
},
75-
];
94+
export type ConnectQueryKey<OutputMessage extends DescMessage = DescMessage> =
95+
| InfiniteConnectQueryKey<OutputMessage>
96+
| FiniteConnectQueryKey<OutputMessage>
97+
| [
98+
"connect-query",
99+
SharedConnectQueryOptions & {
100+
cardinality: undefined;
101+
},
102+
];
76103

77104
type KeyParamsForMethod<Desc extends DescMethod> = {
78105
/**
@@ -152,14 +179,48 @@ type KeyParamsForService<Desc extends DescService> = {
152179
*
153180
* @see ConnectQueryKey for information on the components of Connect-Query's keys.
154181
*/
182+
export function createConnectQueryKey<
183+
I extends DescMessage,
184+
O extends DescMessage,
185+
>(
186+
params: KeyParamsForMethod<DescMethodUnary<I, O>> & {
187+
cardinality: "finite";
188+
},
189+
): FiniteConnectQueryKey<O>;
190+
export function createConnectQueryKey<
191+
I extends DescMessage,
192+
O extends DescMessage,
193+
>(
194+
params: KeyParamsForMethod<DescMethodUnary<I, O>> & {
195+
cardinality: "infinite";
196+
},
197+
): InfiniteConnectQueryKey<O>;
198+
export function createConnectQueryKey<
199+
I extends DescMessage,
200+
O extends DescMessage,
201+
>(
202+
params: KeyParamsForMethod<DescMethodUnary<I, O>> & {
203+
cardinality: undefined;
204+
},
205+
): ConnectQueryKey<O>;
206+
export function createConnectQueryKey<
207+
O extends DescMessage,
208+
Desc extends DescService,
209+
>(params: KeyParamsForService<Desc>): ConnectQueryKey<O>;
155210
export function createConnectQueryKey<
156211
I extends DescMessage,
157212
O extends DescMessage,
158213
Desc extends DescService,
159214
>(
160215
params: KeyParamsForMethod<DescMethodUnary<I, O>> | KeyParamsForService<Desc>,
161-
): ConnectQueryKey {
162-
const props: ConnectQueryKey[1] =
216+
): ConnectQueryKey<O> {
217+
const props: {
218+
serviceName: string;
219+
methodName?: string;
220+
transport?: string;
221+
cardinality?: "finite" | "infinite";
222+
input?: "skipped" | Record<string, unknown>;
223+
} =
163224
params.schema.kind == "rpc"
164225
? {
165226
serviceName: params.schema.parent.typeName,
@@ -185,5 +246,5 @@ export function createConnectQueryKey<
185246
);
186247
}
187248
}
188-
return ["connect-query", props];
249+
return ["connect-query", props] as ConnectQueryKey<O>;
189250
}

0 commit comments

Comments
 (0)