From d1b09b6fb4bfafe9a0d1641d3f18eb47ea2c6725 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Wed, 6 May 2026 20:39:54 -0500 Subject: [PATCH 1/3] define `kind` to discriminate tags, require each kind is satisfied by setQueryData handler --- src/createQueryClient.spec-d.ts | 95 ++++++++++++++++++++++++++------- src/createQueryClient.spec.ts | 22 ++++++++ src/createQueryClient.ts | 20 ++++--- src/tag.spec-d.ts | 20 +++++-- src/tag.ts | 24 +++++---- src/types/client.ts | 16 ++++-- src/types/tags.ts | 20 +++++-- src/types/utilities.ts | 3 ++ 8 files changed, 175 insertions(+), 45 deletions(-) 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 From d354625ea14e29c13ac65bf7c39bfa184aea2754 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Fri, 8 May 2026 16:21:54 -0500 Subject: [PATCH 2/3] move abstraction into tag itself, tags support multiple kinds --- src/createQueryClient.spec-d.ts | 147 ++++++------------ src/createQueryClient.spec.ts | 68 +++----- src/createQueryClient.ts | 36 ++--- src/createQueryGroup.spec.ts | 14 +- src/createQueryGroupTags.ts | 49 +++--- src/createUseQuery.ts | 4 +- src/getAllTags.spec.ts | 10 +- src/tag.spec-d.ts | 54 ++++--- src/tag.spec.ts | 31 ++-- src/tag.ts | 39 +++-- src/types/client.ts | 18 +-- src/types/query.ts | 4 +- src/types/tags.ts | 47 +++--- src/utilities/createDefinedMutationOptions.ts | 26 +++- src/utilities/createMutationOptions.ts | 14 +- 15 files changed, 253 insertions(+), 308 deletions(-) diff --git a/src/createQueryClient.spec-d.ts b/src/createQueryClient.spec-d.ts index b452ae2..97b7467 100644 --- a/src/createQueryClient.spec-d.ts +++ b/src/createQueryClient.spec-d.ts @@ -26,25 +26,24 @@ describe('query', () => { test('tags', async () => { const action = () => 'response' const { query, setQueryData } = createQueryClient() - const numberTag = tag('count') - const stringTag = tag('name') + const stringTag = tag().add('name') const untypedTag = tag() - // @ts-expect-error - number tag not assignable to string action - query(action, [], { tags: [numberTag, stringTag] }) - - // @ts-expect-error - number tag not assignable to string action - query(action, [], { tags: () => [numberTag, stringTag] }) - query(action, [], { tags: [stringTag, untypedTag] }) query(action, [], { tags: () => [stringTag, untypedTag] }) query(action, [], { tags: [untypedTag] }) query(action, [], { tags: () => [untypedTag] }) - setQueryData([numberTag, stringTag], { - count: (data) => data + 1, - name: (data) => data + 'bar', + setQueryData(stringTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return data + 'bar' + }) + + // @ts-expect-error - sharedTag has data: never, can't return anything useful + setQueryData(untypedTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + return 'could be corrupting' }) }) }) @@ -104,113 +103,63 @@ describe('defineQuery', () => { }) describe('setQueryData', () => { - test('tags', async () => { + test('naked tag is not setQueryData-able (data: never)', () => { const { setQueryData } = createQueryClient() - const numberTag = tag('count') - const stringTag = tag('name') - const untypedTag = tag() - - setQueryData(untypedTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' - }) - - setQueryData(numberTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 2 - }) + const myTag = tag() - setQueryData(stringTag, (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'new string' - }) + // @ts-expect-error - data: never, return must be never (effectively impossible) + setQueryData(myTag, () => 'anything') + }) - setQueryData([untypedTag], (data) => { - expectTypeOf(data).toEqualTypeOf() - return 'foo' - }) + test('typed tag passes data through with its declared type', () => { + const { setQueryData } = createQueryClient() + const numberTag = tag() - setQueryData([numberTag], (data) => { + setQueryData(numberTag, (data) => { expectTypeOf(data).toEqualTypeOf() - return 2 + return data + 1 }) - // 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' - }, - }) - - // 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 with string return + // @ts-expect-error - returning wrong type setQueryData(numberTag, (data) => { expectTypeOf(data).toEqualTypeOf() - return 'string' - }) - - // @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, + return 'wrong type' }) }) - test('tags with shared kind', () => { + test('descendant tag has its own data type', () => { const { setQueryData } = createQueryClient() - const userTagA = tag<{ id: number }, 'user'>('user') - const userTagB = tag<{ id: number }, 'user'>('user') + const sharedTag = tag() + const userTag = sharedTag.add<{ id: number }, 'user'>('user') - // same kind, same type — simple callback works - setQueryData([userTagA, userTagB], (data) => { + setQueryData(userTag, (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 - }, + // ancestor tag is still locked at the type level + // @ts-expect-error - sharedTag has data: never + setQueryData(sharedTag, (data) => { + expectTypeOf(data).toEqualTypeOf() + + return 'could be corrupting' }) }) - test('tags with same kind but different types collapse data to never', () => { + test('descendants nest arbitrarily deep', () => { const { setQueryData } = createQueryClient() - const aTag = tag('shared') - const bTag = tag('shared') + const sharedTag = tag() + const userTag = sharedTag.add<{ id: number }, 'user'>('user') + const userAvatarTag = userTag.add<{ id: number, url: string }, 'avatar'>('avatar') - setQueryData([aTag, bTag], { - shared: (data) => { - expectTypeOf(data).toEqualTypeOf() - return data - }, + setQueryData(userAvatarTag, (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number, url: string }>() + return data + }) + + setQueryData(userTag, (data) => { + expectTypeOf(data).toEqualTypeOf<{ id: number }>() + return data }) }) @@ -311,12 +260,14 @@ describe('refreshQueryData', () => { test('tags', () => { const { refreshQueryData } = createQueryClient() - const numberTag = tag() - const stringTag = tag() + const sharedTag = tag() + const numberTag = sharedTag.add('count') + const stringTag = sharedTag.add('name') const action = (param: number) => param + refreshQueryData(sharedTag) refreshQueryData(numberTag) - refreshQueryData([numberTag, stringTag]) + refreshQueryData(stringTag) refreshQueryData(action) refreshQueryData(action, [2]) diff --git a/src/createQueryClient.spec.ts b/src/createQueryClient.spec.ts index 056b5a0..ad6c4a3 100644 --- a/src/createQueryClient.spec.ts +++ b/src/createQueryClient.spec.ts @@ -562,22 +562,18 @@ describe('setQueryData', () => { await vi.runOnlyPendingTimersAsync() - setQueryData(stringTag, () => { - return 'bar' - }) - - setQueryData(numberTag, () => { - return 2 - }) + setQueryData(stringTag, () => 'bar') + setQueryData(numberTag, () => 2) expect(stringQuery.data).toBe('bar') expect(numberQuery.data).toBe(2) }) - test('tags', async () => { + test('descendant tag setter only matches that descendant\'s queries', async () => { const { setQueryData, query } = createQueryClient() - const stringTag = tag() - const numberTag = tag() + const sharedTag = tag() + const stringTag = sharedTag.add('name') + const numberTag = sharedTag.add('count') const stringAction = () => 'foo' const numberAction = () => 1 @@ -587,38 +583,10 @@ describe('setQueryData', () => { await vi.runOnlyPendingTimersAsync() - setQueryData([stringTag], () => { - return 'bar' - }) - - setQueryData([numberTag], () => { - return 2 - }) - - expect(stringQuery.data).toBe('bar') - 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, - }) + setQueryData(stringTag, (data) => data + '-bar') expect(stringQuery.data).toBe('foo-bar') - expect(numberQuery.data).toBe(11) + expect(numberQuery.data).toBe(1) }) test('action', async () => { @@ -678,8 +646,8 @@ describe('refreshQueryData', () => { const numberAction = vi.fn() const stringAction = vi.fn() - const numberTag = tag() - const stringTag = tag() + const numberTag = tag() + const stringTag = tag() query(numberAction, [], { tags: [numberTag] }) query(stringAction, [], { tags: [stringTag] }) @@ -817,7 +785,7 @@ describe('mutate', () => { [undefined], ])('refreshes tagged queries: %s', async (refreshQueryData) => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn() @@ -839,7 +807,7 @@ describe('mutate', () => { test('does not refresh tagged queries if refreshQueryData is false', async () => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn() @@ -861,7 +829,7 @@ describe('mutate', () => { test('does not refresh tagged queries if the action throws an error', async () => { const { mutate, query } = createQueryClient() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn(() => { throw new Error() @@ -1069,7 +1037,7 @@ describe('useMutation', () => { test('setQueryDataBefore and setQueryDataAfter are called when the mutation is executed', async () => { const { useMutation, query } = createQueryClient() const { promise, resolve } = Promise.withResolvers() - const numberTag = tag() + const numberTag = tag() const queryAction = vi.fn() const mutationAction = vi.fn(() => promise) const setQueryDataBefore = vi.fn() @@ -1328,8 +1296,8 @@ describe('defineMutation', () => { const { promise, resolve } = Promise.withResolvers() const mutationPayload = 1 const mutationAction = (value: number) => promise.then(() => value) - const tagA = tag() - const tagB = tag() + const tagA = tag() + const tagB = tag() const queryAResponse = 1 const queryBResponse = 1 const queryAAction = () => queryAResponse @@ -1649,8 +1617,8 @@ describe('defineMutation', () => { const { promise, resolve } = Promise.withResolvers() const mutationPayload = 1 const mutationAction = (value: number) => promise.then(() => value) - const tagA = tag() - const tagB = tag() + const tagA = tag() + const tagB = tag() const queryAResponse = 1 const queryBResponse = 1 const queryAAction = () => queryAResponse diff --git a/src/createQueryClient.ts b/src/createQueryClient.ts index 72d6f5b..9a653de 100644 --- a/src/createQueryClient.ts +++ b/src/createQueryClient.ts @@ -13,7 +13,7 @@ import { } from './types/client' import { createQueryGroups } from './createQueryGroups' import { createUseQuery } from './createUseQuery' -import { isQueryTag, isQueryTags, QueryTag } from './types/tags' +import { isQueryTag, QueryTag } from './types/tags' import { isArray } from './utilities/arrays' import { assertNever } from './utilities/assert' import { QueryGroup } from './createQueryGroup' @@ -59,8 +59,8 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } const setQueryData: SetQueryData = ( - param1: QueryTag | QueryTag[] | QueryAction, - param2: Parameters | QueryDataSetter | Record, + param1: QueryTag | QueryAction, + param2: Parameters | QueryDataSetter, param3?: QueryDataSetter, ): void => { const setDataForGroups = (groups: QueryGroup[], setter: QueryDataSetter): void => { @@ -72,20 +72,12 @@ export function createQueryClient(options?: ClientOptions): QueryClient { }) } - if (isQueryTag(param1) || isQueryTags(param1)) { - 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) - } - } + if (isQueryTag(param1)) { + const queryTag = param1 + const setter = param2 as QueryDataSetter + const groups = getQueryGroups(queryTag) + + setDataForGroups(groups, setter) return } @@ -115,12 +107,12 @@ export function createQueryClient(options?: ClientOptions): QueryClient { } const refreshQueryData: RefreshQueryData = ( - param1: QueryTag | QueryTag[] | QueryAction, + param1: QueryTag | QueryAction, param2?: Parameters, ): void => { - if (isQueryTag(param1) || isQueryTags(param1)) { - const tags = param1 - const groups = getQueryGroups(tags) + if (isQueryTag(param1)) { + const queryTag = param1 + const groups = getQueryGroups(queryTag) groups.forEach((group) => { group.execute() @@ -141,7 +133,7 @@ export function createQueryClient(options?: ClientOptions): QueryClient { return } - assertNever(param1, 'Invalid arguments given to setQueryData') + assertNever(param1, 'Invalid arguments given to refreshQueryData') } const mutate: MutationFunction = (action, parameters, options) => { diff --git a/src/createQueryGroup.spec.ts b/src/createQueryGroup.spec.ts index 25144b0..915c296 100644 --- a/src/createQueryGroup.spec.ts +++ b/src/createQueryGroup.spec.ts @@ -244,31 +244,27 @@ describe('given group with tags', () => { test('can check if it has a tag', async () => { const group = createQueryGroup(vi.fn(), []) const tag1 = tag() - const tag2 = tag((value: string) => value) + const tag2 = tag() expect(group.hasTag(tag1)).toBe(false) const query1 = group.createQuery({ tags: [tag1] }) - const query2 = group.createQuery({ tags: [tag2('foo')] }) + const query2 = group.createQuery({ tags: [tag2] }) - // need executed to happen for tag factories await vi.advanceTimersByTimeAsync(0) expect(group.hasTag(tag1)).toBe(true) - expect(group.hasTag(tag2('foo'))).toBe(true) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(true) query1.dispose() expect(group.hasTag(tag1)).toBe(false) - expect(group.hasTag(tag2('foo'))).toBe(true) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(true) query2.dispose() expect(group.hasTag(tag1)).toBe(false) - expect(group.hasTag(tag2('foo'))).toBe(false) - expect(group.hasTag(tag2('bar'))).toBe(false) + expect(group.hasTag(tag2)).toBe(false) }) }) diff --git a/src/createQueryGroupTags.ts b/src/createQueryGroupTags.ts index 1875e2a..8ff791c 100644 --- a/src/createQueryGroupTags.ts +++ b/src/createQueryGroupTags.ts @@ -2,52 +2,55 @@ import { QueryTag } from './types/tags' import { TagKey } from './getTagKey' export function createQueryGroupTags() { - const tags = new Map>() - const queries = new Map>() + const tagKeyToQueryIds = new Map>() + const queryIdToTags = new Map>() function clear(): void { - tags.clear() - queries.clear() + tagKeyToQueryIds.clear() + queryIdToTags.clear() } function has(tag: QueryTag): boolean { - return tags.has(tag.key) + return tagKeyToQueryIds.has(tag.key) } - function getQueryIdsByTag(tag: QueryTag): Set { - if (!tags.has(tag.key)) { - tags.set(tag.key, new Set()) + function getQueryIdsForKey(key: TagKey): Set { + if (!tagKeyToQueryIds.has(key)) { + tagKeyToQueryIds.set(key, new Set()) } - return tags.get(tag.key)! + return tagKeyToQueryIds.get(key)! } function getTagsByQueryId(queryId: number): Set { - if (!queries.has(queryId)) { - queries.set(queryId, new Set()) + if (!queryIdToTags.has(queryId)) { + queryIdToTags.set(queryId, new Set()) } - return queries.get(queryId)! + return queryIdToTags.get(queryId)! } function addTag(tag: QueryTag, queryId: number): void { - getQueryIdsByTag(tag).add(queryId) + for (const key of tag.keys) { + getQueryIdsForKey(key).add(queryId) + } + getTagsByQueryId(queryId).add(tag) } function removeTag(tag: QueryTag, queryId: number): void { - const queryTags = getQueryIdsByTag(tag) - const tagQueries = getTagsByQueryId(queryId) - - queryTags.delete(queryId) - tagQueries.delete(tag) - - if (queryTags.size === 0) { - tags.delete(tag.key) + for (const key of tag.keys) { + const queryIds = getQueryIdsForKey(key) + queryIds.delete(queryId) + if (queryIds.size === 0) { + tagKeyToQueryIds.delete(key) + } } - if (tagQueries.size === 0) { - queries.delete(queryId) + const tagSet = getTagsByQueryId(queryId) + tagSet.delete(tag) + if (tagSet.size === 0) { + queryIdToTags.delete(queryId) } } diff --git a/src/createUseQuery.ts b/src/createUseQuery.ts index 17e9878..6cc39ba 100644 --- a/src/createUseQuery.ts +++ b/src/createUseQuery.ts @@ -1,7 +1,7 @@ import { CreateQuery } from './createQueryGroups' import { Query, QueryAction, QueryActionArgs } from './types/query' import { onScopeDispose, ref, toRef, toRefs, toValue, watch } from 'vue' -import equal from "fast-deep-equal" +import equal from 'fast-deep-equal' import { isDefined } from './utilities' import { UseQueryOptions } from './types/client' @@ -23,7 +23,7 @@ export function createUseQuery(createQuery: CreateQuery, action: QueryAction, pa watch(() => ({ enabled: enabled.value, parameters: toValue(parameters) }) as const, ({ enabled, parameters }, previous) => { const isSameParameters = previous && isDefined(previous.parameters) && equal(previous.parameters, parameters) - const isSameEnabled = previous && previous.enabled === enabled + const isSameEnabled = previous?.enabled === enabled if (isSameParameters && isSameEnabled) { return diff --git a/src/getAllTags.spec.ts b/src/getAllTags.spec.ts index b932cf4..76e14c4 100644 --- a/src/getAllTags.spec.ts +++ b/src/getAllTags.spec.ts @@ -4,18 +4,18 @@ import { tag } from './tag' const tagA = tag() const tagB = tag() -const tagC = tag((input: string) => input) +const tagC = tag() test('given tags returns all tags', () => { - const tags = getAllTags([tagA, tagB, tagC('foo')], undefined) + const tags = getAllTags([tagA, tagB, tagC], undefined) - expect(tags).toEqual([tagA, tagB, tagC('foo')]) + expect(tags).toEqual([tagA, tagB, tagC]) }) test('given a function returns all tags', () => { - const tags = getAllTags((input: string) => [tagA, tagB, tagC(input)], 'foo') + const tags = getAllTags(() => [tagA, tagB, tagC], 'foo') - expect(tags).toEqual([tagA, tagB, tagC('foo')]) + expect(tags).toEqual([tagA, tagB, tagC]) }) test('given no tags returns an empty array', () => { diff --git a/src/tag.spec-d.ts b/src/tag.spec-d.ts index 4cbf960..b40a328 100644 --- a/src/tag.spec-d.ts +++ b/src/tag.spec-d.ts @@ -1,50 +1,60 @@ import { expectTypeOf, test, vi } from 'vitest' -import { QueryTag, QueryTagFactory, Unset } from '@/types/tags' +import { QueryTag } from '@/types/tags' import { createQueryClient } from './createQueryClient' import { tag } from './tag' -test('tag function returns a tag when no callback is provided', () => { +test('tag function returns a QueryTag', () => { const value = tag() expectTypeOf(value).toExtend() + expectTypeOf(value).toEqualTypeOf>() }) -test('tag function returns a tag factory when a callback is provided', () => { - const factory = tag((string: string) => string) +test('tag() returns a typed QueryTag', () => { + const value = tag() - expectTypeOf(factory).toExtend>() + expectTypeOf(value).toEqualTypeOf>() +}) - const value = factory('foo') +test('tag.add(K) returns a typed descendant', () => { + const baseTag = tag() + const userTag = baseTag.add<{ id: number }, 'user'>('user') - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(userTag).toEqualTypeOf>() }) -test('tag function returns a typed tag when data generic is provided', () => { - const value = tag() +test('descendants nest arbitrarily deep', () => { + const baseTag = tag() + const userTag = baseTag.add<{ id: number }, 'user'>('user') + const userAvatarTag = userTag.add<{ id: number, url: string }, 'avatar'>('avatar') - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(userAvatarTag).toEqualTypeOf>() }) -test('tag factory returns a typed tag when data generic is provided', () => { - const factory = tag((value: string) => value) - - expectTypeOf(factory).toEqualTypeOf>() +test('descendant data must be assignable to ancestor data', () => { + type User = { id: number } + type UserImage = { id: number, url: string } + type UserDetails = { id: number, bio: string } - const value = factory('foo') + const usersTag = tag() - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(usersTag.add('user')).toEqualTypeOf>() + expectTypeOf(usersTag.add('image')).toEqualTypeOf>() + expectTypeOf(usersTag.add('details')).toEqualTypeOf>() }) -test('tag function preserves kind literal', () => { - const value = tag('count') +test('descendant with data not assignable to ancestor is rejected', () => { + const userTag = tag<{ id: number }>() - expectTypeOf(value).toEqualTypeOf>() + // @ts-expect-error - { unrelated: true } is not assignable to { id: number } + userTag.add<{ unrelated: true }, 'bad'>('bad') }) -test('tag function preserves kind literal with explicit data and kind generics', () => { - const value = tag('count') +test('untyped root places no constraint on descendants', () => { + const baseTag = tag() - expectTypeOf(value).toEqualTypeOf>() + expectTypeOf(baseTag.add('count')).toEqualTypeOf>() + expectTypeOf(baseTag.add<{ anything: true }, 'whatever'>('whatever')).toEqualTypeOf>() }) test('query from query function with tags callback is called with the query data', () => { diff --git a/src/tag.spec.ts b/src/tag.spec.ts index e8d9ac9..66056df 100644 --- a/src/tag.spec.ts +++ b/src/tag.spec.ts @@ -8,19 +8,28 @@ test('tags are unique', () => { expect(tag1).not.toBe(tag2) }) -test('tag factories are unique', () => { - const factory1 = tag((string: string) => string) - const factory2 = tag((string: string) => string) - const value1 = factory1('foo') - const value2 = factory2('foo') +test('tag.add returns a new descendant tag', () => { + const baseTag = tag() + const userTag = baseTag.add('count') - expect(value1).not.toBe(value2) + expect(userTag).toBeDefined() + expect(userTag.key).not.toBe(baseTag.key) }) -test('tag factory returns the same key when given the same value', () => { - const factory = tag((string: string) => string) - const value1 = factory('foo') - const value2 = factory('foo') +test('descendants carry their ancestor keys', () => { + const baseTag = tag() + const userTag = baseTag.add('count') - expect(value1.key).toBe(value2.key) + expect(userTag.keys).toContain(baseTag.key) + expect(userTag.keys).toContain(userTag.key) +}) + +test('chained .add calls accumulate ancestor keys', () => { + const baseTag = tag() + const userTag = baseTag.add<{ id: number }, 'user'>('user') + const deepTag = userTag.add<{ id: number, label: string }, 'label'>('label') + + expect(deepTag.keys).toContain(baseTag.key) + expect(deepTag.keys).toContain(userTag.key) + expect(deepTag.keys).toContain(deepTag.key) }) diff --git a/src/tag.ts b/src/tag.ts index 204103f..45203d6 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,30 +1,29 @@ import { createSequence } from './createSequence' -import { getTagKey } from './getTagKey' -import { QueryTagFactory, QueryTagCallback, QueryTag, Unset, unset, DEFAULT_TAG_KIND, DefaultTagKind } from './types/tags' +import { getTagKey, TagKey } from './getTagKey' +import { QueryTag, unset } from './types/tags' const createTagId = createSequence() -function createQueryTag(id: number, kind: string, value: unknown): QueryTag { - return { - data: unset, - kind, - key: getTagKey(id, value), - } as QueryTag -} - -export function tag(kind?: TKind): QueryTag -export function tag(callback: QueryTagCallback, kind?: TKind): QueryTagFactory -export function tag(callbackOrKind?: QueryTagCallback | string, maybeKind?: string): QueryTag | QueryTagFactory { +function createTag(parentKeys: readonly TagKey[], label?: string): QueryTag { const id = createTagId() + const ownKey = getTagKey(id, label) + const keys = Object.freeze([...parentKeys, ownKey]) - if (typeof callbackOrKind === 'function') { - const callback = callbackOrKind - const kind = maybeKind ?? DEFAULT_TAG_KIND - - return (value) => createQueryTag(id, kind, callback(value)) + const queryTag = { + data: unset, + key: ownKey, + keys, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + add(name: TKind): QueryTag { + return createTag(keys, name) + }, } - const kind = callbackOrKind ?? DEFAULT_TAG_KIND + return queryTag as unknown as QueryTag +} - return createQueryTag(id, kind, undefined) +export function tag(): QueryTag +export function tag(): QueryTag +export function tag(): QueryTag { + return createTag([]) } diff --git a/src/types/client.ts b/src/types/client.ts index 08c5d81..1738ba1 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, QueryTagKind } from './tags' -import { DefaultValue, UnionToIntersection } from './utilities' +import { QueryTag, QueryTagType } from './tags' +import { DefaultValue } from './utilities' export type QueryClient = { query: QueryFunction, @@ -62,24 +62,14 @@ 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: SetQueryDataValue): void, + (tag: TQueryTag, setter: QueryDataSetter>): void, (action: TAction, setter: QueryDataSetter>): void, (action: TAction, parameters: Parameters, setter: QueryDataSetter>): void, } export type RefreshQueryData = { - (tag: TQueryTag | TQueryTag[]): void, + (tag: QueryTag): void, (action: QueryAction): void, (action: TAction, parameters: Parameters): void, } diff --git a/src/types/query.ts b/src/types/query.ts index a3a5cb7..0368279 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -1,6 +1,6 @@ import { RetryOptions } from '@/utilities/retry' import { Getter } from './getters' -import { QueryTag, Unset } from '@/types/tags' +import { QueryTag } from '@/types/tags' import { DefaultValue } from './utilities' export type QueryAction = (...args: any[]) => any @@ -19,7 +19,7 @@ export type QueryActionArgs< export type QueryTags< TAction extends QueryAction = QueryAction -> = QueryTag | Unset>[] | ((value: QueryData) => QueryTag | Unset>[]) +> = QueryTag>[] | ((value: QueryData) => QueryTag>[]) export type QueryOptions< TAction extends QueryAction = QueryAction, diff --git a/src/types/tags.ts b/src/types/tags.ts index 2dc479e..c40e2b3 100644 --- a/src/types/tags.ts +++ b/src/types/tags.ts @@ -3,35 +3,41 @@ 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, - TKind extends string = string -> = { +export type QueryTag = { /** * @private * @internal - * This property is unused, but necessary to preserve the type for TData because unused generics are ignored by typescript. + * Phantom field used purely to preserve the TData generic in the type; + * the runtime value is always the `unset` symbol regardless of TData. */ data: TData, - kind: TKind, + /** + * The tag's own unique key. + */ key: TagKey, + /** + * Own key plus all ancestor keys, in root-to-leaf order. A query tagged + * with this tag is registered against every key in this list, so + * setQueryData / invalidateQueries on any ancestor matches the query. + */ + keys: readonly TagKey[], + /** + * Create a typed descendant of this tag. The descendant inherits this + * tag's identity, so any operation against this tag also matches queries + * tagged with the descendant. The descendant's data type must be assignable + * to this tag's data type, so the parent acts as a supertype of all + * descendants. An untyped root (`tag()`) places no constraint on descendants. + */ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters + add: (name: TKind) => QueryTag, } -export type QueryTagType = TQueryTag extends QueryTag - ? TData extends Unset - ? unknown - : TData - : never - -export type QueryTagKind = TQueryTag extends QueryTag - ? TKind +export type QueryTagType = TQueryTag extends QueryTag + ? TData : never export function isQueryTag(tag: unknown): tag is QueryTag { - return typeof tag === 'object' && tag !== null && 'data' in tag && 'kind' in tag && 'key' in tag + return typeof tag === 'object' && tag !== null && 'data' in tag && 'key' in tag && 'keys' in tag } export function isQueryTags(tags: unknown): tags is QueryTag[] { @@ -44,6 +50,5 @@ export type QueryTagCallback< export type QueryTagFactory< TData = unknown, - TInput = unknown, - TKind extends string = string -> = (value: TInput) => QueryTag + TInput = unknown +> = (value: TInput) => QueryTag diff --git a/src/utilities/createDefinedMutationOptions.ts b/src/utilities/createDefinedMutationOptions.ts index 047ecea..a435b70 100644 --- a/src/utilities/createDefinedMutationOptions.ts +++ b/src/utilities/createDefinedMutationOptions.ts @@ -47,14 +47,18 @@ export function createDefinedMutationOptions({ const tags = getAllTags(options?.tags, undefined) const setter = (data: QueryData) => setQueryDataBefore(data, context) - setQueryData(tags, setter) + for (const tag of tags) { + setQueryData(tag, setter) + } } if (definedSetQueryDataBefore) { const tags = getAllTags(definedOptions?.tags, undefined) const setter = (data: QueryData) => definedSetQueryDataBefore(data, context) - setQueryData(tags, setter) + for (const tag of tags) { + setQueryData(tag, setter) + } } onExecute?.(context) @@ -66,16 +70,26 @@ export function createDefinedMutationOptions({ const definedTags = getAllTags(definedOptions?.tags, context.data) if (shouldRefreshQueryData) { - refreshQueryData(tags) - refreshQueryData(definedTags) + for (const tag of tags) { + refreshQueryData(tag) + } + for (const tag of definedTags) { + refreshQueryData(tag) + } } if (setQueryDataAfter) { - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } if (definedSetQueryDataAfter) { - setQueryData(definedTags, (queryData: QueryData): QueryData => definedSetQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => definedSetQueryDataAfter(queryData, context) + for (const tag of definedTags) { + setQueryData(tag, setter) + } } onSuccess?.(context) diff --git a/src/utilities/createMutationOptions.ts b/src/utilities/createMutationOptions.ts index e7e726b..27d7ab2 100644 --- a/src/utilities/createMutationOptions.ts +++ b/src/utilities/createMutationOptions.ts @@ -26,7 +26,10 @@ export function createMutationOptions({ options, setQueryData, refreshQueryData payload: context.payload, } satisfies MutationTagsBeforeContext) - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataBefore(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataBefore(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } onExecute?.(context) @@ -39,11 +42,16 @@ export function createMutationOptions({ options, setQueryData, refreshQueryData } satisfies MutationTagsAfterContext) if (options?.refreshQueryData ?? true) { - refreshQueryData(tags) + for (const tag of tags) { + refreshQueryData(tag) + } } if (setQueryDataAfter) { - setQueryData(tags, (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context)) + const setter = (queryData: QueryData): QueryData => setQueryDataAfter(queryData, context) + for (const tag of tags) { + setQueryData(tag, setter) + } } onSuccess?.(context) From 54c56a696c9d06322b4d6999778d85a77711b1f8 Mon Sep 17 00:00:00 2001 From: Evan Sutherland Date: Fri, 8 May 2026 16:30:16 -0500 Subject: [PATCH 3/3] drop keys from .add function --- src/createQueryClient.spec-d.ts | 12 ++++++------ src/createQueryClient.spec.ts | 4 ++-- src/tag.spec-d.ts | 20 ++++++++++---------- src/tag.spec.ts | 8 ++++---- src/tag.ts | 9 ++++----- src/types/tags.ts | 3 +-- 6 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/createQueryClient.spec-d.ts b/src/createQueryClient.spec-d.ts index 97b7467..ca9a1b6 100644 --- a/src/createQueryClient.spec-d.ts +++ b/src/createQueryClient.spec-d.ts @@ -26,7 +26,7 @@ describe('query', () => { test('tags', async () => { const action = () => 'response' const { query, setQueryData } = createQueryClient() - const stringTag = tag().add('name') + const stringTag = tag().add() const untypedTag = tag() query(action, [], { tags: [stringTag, untypedTag] }) @@ -130,7 +130,7 @@ describe('setQueryData', () => { test('descendant tag has its own data type', () => { const { setQueryData } = createQueryClient() const sharedTag = tag() - const userTag = sharedTag.add<{ id: number }, 'user'>('user') + const userTag = sharedTag.add<{ id: number }>() setQueryData(userTag, (data) => { expectTypeOf(data).toEqualTypeOf<{ id: number }>() @@ -149,8 +149,8 @@ describe('setQueryData', () => { test('descendants nest arbitrarily deep', () => { const { setQueryData } = createQueryClient() const sharedTag = tag() - const userTag = sharedTag.add<{ id: number }, 'user'>('user') - const userAvatarTag = userTag.add<{ id: number, url: string }, 'avatar'>('avatar') + const userTag = sharedTag.add<{ id: number }>() + const userAvatarTag = userTag.add<{ id: number, url: string }>() setQueryData(userAvatarTag, (data) => { expectTypeOf(data).toEqualTypeOf<{ id: number, url: string }>() @@ -261,8 +261,8 @@ describe('refreshQueryData', () => { const { refreshQueryData } = createQueryClient() const sharedTag = tag() - const numberTag = sharedTag.add('count') - const stringTag = sharedTag.add('name') + const numberTag = sharedTag.add() + const stringTag = sharedTag.add() const action = (param: number) => param refreshQueryData(sharedTag) diff --git a/src/createQueryClient.spec.ts b/src/createQueryClient.spec.ts index ad6c4a3..9d2772a 100644 --- a/src/createQueryClient.spec.ts +++ b/src/createQueryClient.spec.ts @@ -572,8 +572,8 @@ describe('setQueryData', () => { test('descendant tag setter only matches that descendant\'s queries', async () => { const { setQueryData, query } = createQueryClient() const sharedTag = tag() - const stringTag = sharedTag.add('name') - const numberTag = sharedTag.add('count') + const stringTag = sharedTag.add() + const numberTag = sharedTag.add() const stringAction = () => 'foo' const numberAction = () => 1 diff --git a/src/tag.spec-d.ts b/src/tag.spec-d.ts index b40a328..f1b783b 100644 --- a/src/tag.spec-d.ts +++ b/src/tag.spec-d.ts @@ -16,17 +16,17 @@ test('tag() returns a typed QueryTag', () => { expectTypeOf(value).toEqualTypeOf>() }) -test('tag.add(K) returns a typed descendant', () => { +test('tag.add() returns a typed descendant', () => { const baseTag = tag() - const userTag = baseTag.add<{ id: number }, 'user'>('user') + const userTag = baseTag.add<{ id: number }>() expectTypeOf(userTag).toEqualTypeOf>() }) test('descendants nest arbitrarily deep', () => { const baseTag = tag() - const userTag = baseTag.add<{ id: number }, 'user'>('user') - const userAvatarTag = userTag.add<{ id: number, url: string }, 'avatar'>('avatar') + const userTag = baseTag.add<{ id: number }>() + const userAvatarTag = userTag.add<{ id: number, url: string }>() expectTypeOf(userAvatarTag).toEqualTypeOf>() }) @@ -38,23 +38,23 @@ test('descendant data must be assignable to ancestor data', () => { const usersTag = tag() - expectTypeOf(usersTag.add('user')).toEqualTypeOf>() - expectTypeOf(usersTag.add('image')).toEqualTypeOf>() - expectTypeOf(usersTag.add('details')).toEqualTypeOf>() + expectTypeOf(usersTag.add()).toEqualTypeOf>() + expectTypeOf(usersTag.add()).toEqualTypeOf>() + expectTypeOf(usersTag.add()).toEqualTypeOf>() }) test('descendant with data not assignable to ancestor is rejected', () => { const userTag = tag<{ id: number }>() // @ts-expect-error - { unrelated: true } is not assignable to { id: number } - userTag.add<{ unrelated: true }, 'bad'>('bad') + userTag.add<{ unrelated: true }>() }) test('untyped root places no constraint on descendants', () => { const baseTag = tag() - expectTypeOf(baseTag.add('count')).toEqualTypeOf>() - expectTypeOf(baseTag.add<{ anything: true }, 'whatever'>('whatever')).toEqualTypeOf>() + expectTypeOf(baseTag.add()).toEqualTypeOf>() + expectTypeOf(baseTag.add<{ anything: true }>()).toEqualTypeOf>() }) test('query from query function with tags callback is called with the query data', () => { diff --git a/src/tag.spec.ts b/src/tag.spec.ts index 66056df..f345056 100644 --- a/src/tag.spec.ts +++ b/src/tag.spec.ts @@ -10,7 +10,7 @@ test('tags are unique', () => { test('tag.add returns a new descendant tag', () => { const baseTag = tag() - const userTag = baseTag.add('count') + const userTag = baseTag.add() expect(userTag).toBeDefined() expect(userTag.key).not.toBe(baseTag.key) @@ -18,7 +18,7 @@ test('tag.add returns a new descendant tag', () => { test('descendants carry their ancestor keys', () => { const baseTag = tag() - const userTag = baseTag.add('count') + const userTag = baseTag.add() expect(userTag.keys).toContain(baseTag.key) expect(userTag.keys).toContain(userTag.key) @@ -26,8 +26,8 @@ test('descendants carry their ancestor keys', () => { test('chained .add calls accumulate ancestor keys', () => { const baseTag = tag() - const userTag = baseTag.add<{ id: number }, 'user'>('user') - const deepTag = userTag.add<{ id: number, label: string }, 'label'>('label') + const userTag = baseTag.add<{ id: number }>() + const deepTag = userTag.add<{ id: number, label: string }>() expect(deepTag.keys).toContain(baseTag.key) expect(deepTag.keys).toContain(userTag.key) diff --git a/src/tag.ts b/src/tag.ts index 45203d6..49c4f9d 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -4,18 +4,17 @@ import { QueryTag, unset } from './types/tags' const createTagId = createSequence() -function createTag(parentKeys: readonly TagKey[], label?: string): QueryTag { +function createTag(parentKeys: readonly TagKey[]): QueryTag { const id = createTagId() - const ownKey = getTagKey(id, label) + const ownKey = getTagKey(id, undefined) const keys = Object.freeze([...parentKeys, ownKey]) const queryTag = { data: unset, key: ownKey, keys, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - add(name: TKind): QueryTag { - return createTag(keys, name) + add(): QueryTag { + return createTag(keys) }, } diff --git a/src/types/tags.ts b/src/types/tags.ts index c40e2b3..f7c2f77 100644 --- a/src/types/tags.ts +++ b/src/types/tags.ts @@ -28,8 +28,7 @@ export type QueryTag = { * to this tag's data type, so the parent acts as a supertype of all * descendants. An untyped root (`tag()`) places no constraint on descendants. */ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters - add: (name: TKind) => QueryTag, + add: () => QueryTag, } export type QueryTagType = TQueryTag extends QueryTag