diff --git a/src/createQueryClient.spec-d.ts b/src/createQueryClient.spec-d.ts index 866e4c5..b452ae2 100644 --- a/src/createQueryClient.spec-d.ts +++ b/src/createQueryClient.spec-d.ts @@ -25,9 +25,9 @@ describe('query', () => { test('tags', async () => { const action = () => 'response' - const { query } = createQueryClient() - const numberTag = tag() - const stringTag = tag() + const { query, setQueryData } = createQueryClient() + const numberTag = tag('count') + const stringTag = tag('name') const untypedTag = tag() // @ts-expect-error - number tag not assignable to string action @@ -41,6 +41,11 @@ describe('query', () => { query(action, [], { tags: [untypedTag] }) query(action, [], { tags: () => [untypedTag] }) + + setQueryData([numberTag, stringTag], { + count: (data) => data + 1, + name: (data) => data + 'bar', + }) }) }) }) @@ -101,8 +106,8 @@ describe('defineQuery', () => { describe('setQueryData', () => { test('tags', async () => { const { setQueryData } = createQueryClient() - const numberTag = tag() - const stringTag = tag() + const numberTag = tag('count') + const stringTag = tag('name') const untypedTag = tag() setQueryData(untypedTag, (data) => { @@ -130,28 +135,82 @@ describe('setQueryData', () => { return 2 }) - // this is kinda interesting, no matter the data the return type is the union :thinking: - // so there's not really a type safe way to update multiple queries at once - setQueryData([numberTag, stringTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + // multiple tags with distinct kinds: simple callback collapses to never, must use object form + setQueryData([numberTag, stringTag], { + count: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 1 + }, + name: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 'bar' + }, }) - setQueryData([untypedTag, stringTag, numberTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' + // untyped tag has the default kind, so its handler is keyed by 'default' + setQueryData([untypedTag, stringTag, numberTag], { + default: (data) => { + expectTypeOf(data).toEqualTypeOf() + return 'foo' + }, + count: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + }, + name: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + }, }) - // @ts-expect-error - number tag not assignable to string action + // @ts-expect-error - number tag with string return setQueryData(numberTag, (data) => { expectTypeOf(data).toEqualTypeOf() return 'string' }) - // @ts-expect-error - number tag not assignable to string action - setQueryData([numberTag, stringTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return [] + // @ts-expect-error - object handler returning wrong type for kind + setQueryData([numberTag, stringTag], { + count: () => 'wrong', + name: (data) => data, + }) + + // @ts-expect-error - missing handler for one of the kinds + setQueryData([numberTag, stringTag], { + count: (data) => data, + }) + }) + + test('tags with shared kind', () => { + const { setQueryData } = createQueryClient() + const userTagA = tag<{ id: number }, 'user'>('user') + const userTagB = tag<{ id: number }, 'user'>('user') + + // same kind, same type — simple callback works + setQueryData([userTagA, userTagB], (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number }>() + return data + }) + + // same kind, same type — object form also works + setQueryData([userTagA, userTagB], { + user: (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number }>() + return data + }, + }) + }) + + test('tags with same kind but different types collapse data to never', () => { + const { setQueryData } = createQueryClient() + const aTag = tag('shared') + const bTag = tag('shared') + + setQueryData([aTag, bTag], { + shared: (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + }, }) }) diff --git a/src/createQueryClient.spec.ts b/src/createQueryClient.spec.ts index 484ff8c..056b5a0 100644 --- a/src/createQueryClient.spec.ts +++ b/src/createQueryClient.spec.ts @@ -599,6 +599,28 @@ describe('setQueryData', () => { expect(numberQuery.data).toBe(2) }) + test('tags with object setter dispatches by kind', async () => { + const { setQueryData, query } = createQueryClient() + const stringTag = tag('name') + const numberTag = tag('count') + + const stringAction = () => 'foo' + const numberAction = () => 1 + + const stringQuery = query(stringAction, [], { tags: [stringTag] }) + const numberQuery = query(numberAction, [], { tags: [numberTag] }) + + await vi.runOnlyPendingTimersAsync() + + setQueryData([stringTag, numberTag], { + name: (data) => data + '-bar', + count: (data) => data + 10, + }) + + expect(stringQuery.data).toBe('foo-bar') + expect(numberQuery.data).toBe(11) + }) + test('action', async () => { const { setQueryData, query } = createQueryClient() diff --git a/src/createQueryClient.ts b/src/createQueryClient.ts index 855a6f5..72d6f5b 100644 --- a/src/createQueryClient.ts +++ b/src/createQueryClient.ts @@ -60,7 +60,7 @@ export function createQueryClient(options?: ClientOptions): QueryClient { const setQueryData: SetQueryData = ( param1: QueryTag | QueryTag[] | QueryAction, - param2: Parameters | QueryDataSetter, + param2: Parameters | QueryDataSetter | Record, param3?: QueryDataSetter, ): void => { const setDataForGroups = (groups: QueryGroup[], setter: QueryDataSetter): void => { @@ -73,11 +73,19 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } if (isQueryTag(param1) || isQueryTags(param1)) { - const tags = param1 - const setter = param2 as QueryDataSetter - const groups = getQueryGroups(tags) - - setDataForGroups(groups, setter) + const tags = isArray(param1) ? param1 : [param1] + const setter = param2 as QueryDataSetter | Record + + if (typeof setter === 'function') { + const groups = getQueryGroups(tags) + setDataForGroups(groups, setter) + } else { + for (const tag of tags) { + const handler = setter[tag.kind] + const groups = getQueryGroups(tag) + setDataForGroups(groups, handler) + } + } return } diff --git a/src/tag.spec-d.ts b/src/tag.spec-d.ts index 312f569..4cbf960 100644 --- a/src/tag.spec-d.ts +++ b/src/tag.spec-d.ts @@ -16,23 +16,35 @@ test('tag function returns a tag factory when a callback is provided', () => { const value = factory('foo') - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(value).toEqualTypeOf>() }) test('tag function returns a typed tag when data generic is provided', () => { const value = tag() - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(value).toEqualTypeOf>() }) test('tag factory returns a typed tag when data generic is provided', () => { const factory = tag((value: string) => value) - expectTypeOf(factory).toEqualTypeOf>() + expectTypeOf(factory).toEqualTypeOf>() const value = factory('foo') - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(value).toEqualTypeOf>() +}) + +test('tag function preserves kind literal', () => { + const value = tag('count') + + expectTypeOf(value).toEqualTypeOf>() +}) + +test('tag function preserves kind literal with explicit data and kind generics', () => { + const value = tag('count') + + expectTypeOf(value).toEqualTypeOf>() }) test('query from query function with tags callback is called with the query data', () => { diff --git a/src/tag.ts b/src/tag.ts index 1acb2d9..204103f 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,24 +1,30 @@ import { createSequence } from './createSequence' import { getTagKey } from './getTagKey' -import { QueryTagFactory, QueryTagCallback, QueryTag, Unset, unset } from './types/tags' +import { QueryTagFactory, QueryTagCallback, QueryTag, Unset, unset, DEFAULT_TAG_KIND, DefaultTagKind } from './types/tags' const createTagId = createSequence() -function createQueryTag(id: number, value: unknown): QueryTag { +function createQueryTag(id: number, kind: string, value: unknown): QueryTag { return { data: unset, + kind, key: getTagKey(id, value), - } + } as QueryTag } -export function tag(): QueryTag -export function tag(callback: QueryTagCallback): QueryTagFactory -export function tag(callback?: QueryTagCallback): QueryTag | QueryTagFactory { +export function tag(kind?: TKind): QueryTag +export function tag(callback: QueryTagCallback, kind?: TKind): QueryTagFactory +export function tag(callbackOrKind?: QueryTagCallback | string, maybeKind?: string): QueryTag | QueryTagFactory { const id = createTagId() - if (callback) { - return (value) => createQueryTag(id, callback(value)) + if (typeof callbackOrKind === 'function') { + const callback = callbackOrKind + const kind = maybeKind ?? DEFAULT_TAG_KIND + + return (value) => createQueryTag(id, kind, callback(value)) } - return createQueryTag(id, undefined) + const kind = callbackOrKind ?? DEFAULT_TAG_KIND + + return createQueryTag(id, kind, undefined) } diff --git a/src/types/client.ts b/src/types/client.ts index 768e885..08c5d81 100644 --- a/src/types/client.ts +++ b/src/types/client.ts @@ -1,7 +1,7 @@ import { MutationFunction, MutationComposition, DefineMutation } from './mutation' import { Query, QueryOptions, QueryAction, QueryActionArgs, QueryData } from './query' -import { QueryTag, QueryTagType } from './tags' -import { DefaultValue } from './utilities' +import { QueryTag, QueryTagType, QueryTagKind } from './tags' +import { DefaultValue, UnionToIntersection } from './utilities' export type QueryClient = { query: QueryFunction, @@ -62,8 +62,18 @@ export type DefinedQuery< export type QueryDataSetter = (data: T) => T +type SetQueryDataSimpleData = + UnionToIntersection : never> + +type SetQueryDataKindData = + UnionToIntersection ? QueryTagType : never> + +export type SetQueryDataValue = + | QueryDataSetter> + | { [K in QueryTagKind]: QueryDataSetter> } + export type SetQueryData = { - (tag: TQueryTag | TQueryTag[], setter: QueryDataSetter>): void, + (tag: TQueryTag | TQueryTag[], setter: SetQueryDataValue): void, (action: TAction, setter: QueryDataSetter>): void, (action: TAction, parameters: Parameters, setter: QueryDataSetter>): void, } diff --git a/src/types/tags.ts b/src/types/tags.ts index 892fd30..2dc479e 100644 --- a/src/types/tags.ts +++ b/src/types/tags.ts @@ -3,8 +3,12 @@ import { TagKey } from '@/getTagKey' export const unset = Symbol('unset') export type Unset = typeof unset +export const DEFAULT_TAG_KIND = 'default' +export type DefaultTagKind = typeof DEFAULT_TAG_KIND + export type QueryTag< - TData = unknown + TData = unknown, + TKind extends string = string > = { /** * @private @@ -12,17 +16,22 @@ export type QueryTag< * This property is unused, but necessary to preserve the type for TData because unused generics are ignored by typescript. */ data: TData, + kind: TKind, key: TagKey, } -export type QueryTagType = TQueryTag extends QueryTag +export type QueryTagType = TQueryTag extends QueryTag ? TData extends Unset ? unknown : TData : never +export type QueryTagKind = TQueryTag extends QueryTag + ? TKind + : never + export function isQueryTag(tag: unknown): tag is QueryTag { - return typeof tag === 'object' && tag !== null && 'data' in tag && 'key' in tag + return typeof tag === 'object' && tag !== null && 'data' in tag && 'kind' in tag && 'key' in tag } export function isQueryTags(tags: unknown): tags is QueryTag[] { @@ -35,5 +44,6 @@ export type QueryTagCallback< export type QueryTagFactory< TData = unknown, - TInput = unknown -> = (value: TInput) => QueryTag + TInput = unknown, + TKind extends string = string +> = (value: TInput) => QueryTag diff --git a/src/types/utilities.ts b/src/types/utilities.ts index 24e26ad..3d14cf5 100644 --- a/src/types/utilities.ts +++ b/src/types/utilities.ts @@ -1 +1,4 @@ export type DefaultValue = unknown extends TValue ? TDefault : TValue + +export type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never