1
1
export type QueryKeyPart = string | number | boolean | object | undefined ;
2
- export type QueryKey = QueryKeyPart [ ] ;
3
- export type QueryKeyBuilder < Args extends unknown [ ] = [ ] > = (
4
- ...args : Args
5
- ) => QueryKey ;
6
- export type QueryKeyRegistry = Record < string , QueryKeyBuilder < any > > ;
2
+ export type QueryKey = readonly QueryKeyPart [ ] ;
3
+
4
+ export type QueryKeyBuilder <
5
+ Args extends unknown [ ] = [ ] ,
6
+ Return extends QueryKey = QueryKey
7
+ > = ( ...args : Args ) => Return ;
8
+
9
+ export type QueryKeyRegistry = Record < string , QueryKeyBuilder < any , any > > ;
10
+
11
+ export function createQueryKey < T extends readonly QueryKeyPart [ ] > (
12
+ ...parts : T
13
+ ) : T {
14
+ return parts ;
15
+ }
16
+
17
+ type ValidFunction < T > = T extends ( ...args : any [ ] ) => readonly QueryKeyPart [ ]
18
+ ? T
19
+ : never ;
20
+
21
+ type ValidateKeyMap < T > = {
22
+ [ K in keyof T ] : T [ K ] extends ( ...args : any [ ] ) => any
23
+ ? ValidFunction < T [ K ] >
24
+ : T [ K ] extends object
25
+ ? ValidateKeyMap < T [ K ] >
26
+ : never ;
27
+ } ;
7
28
8
29
export class QueryKeyManager {
9
30
private static registry : QueryKeyRegistry = { } ;
10
31
private static keyNames : Set < string > = new Set ( ) ;
11
32
12
- /**
13
- * Creates a new query key with collision protection
14
- *
15
- * @param name Unique key identifier (dot notation recommended for namespacing)
16
- * @param keyMap Object containing query key builder functions
17
- * @returns Type-safe query key builder functions with proper inference
18
- *
19
- * @example
20
- * const userKeys = QueryKeyManager.create('user', {
21
- * profile: (userId: string) => ['user', 'profile', userId] as const,
22
- * settings: (userId: string, section?: string) => ['user', 'settings', userId, section] as const
23
- * } as const);
24
- *
25
- * // Types are properly inferred:
26
- * // userKeys.profile: (userId: string) => QueryKey
27
- * // userKeys.settings: (userId: string, section?: string) => QueryKey
28
- */
29
33
static create <
30
- const KeyMap extends Record < string , ( ...args : any [ ] ) => QueryKey >
31
- > ( name : string , keyMap : KeyMap ) : KeyMap {
32
- // Runtime duplicate check
34
+ const KeyMap extends Record < string , QueryKeyBuilder < any , any > | object >
35
+ > ( name : string , keyMap : KeyMap & ValidateKeyMap < KeyMap > ) : KeyMap {
33
36
if ( this . keyNames . has ( name ) ) {
34
37
if ( process . env . NODE_ENV !== "production" ) {
35
38
throw new Error ( `QueryKeyManager: Key name "${ name } " already exists` ) ;
@@ -39,33 +42,31 @@ export class QueryKeyManager {
39
42
40
43
this . keyNames . add ( name ) ;
41
44
42
- // Register each key builder (type-erased for storage)
43
- for ( const [ key , builder ] of Object . entries ( keyMap ) ) {
44
- const fullKey = `${ name } .${ key } ` ;
45
- this . registry [ fullKey ] = builder as QueryKeyBuilder < any > ;
46
- }
45
+ const register = ( prefix : string , node : Record < string , any > ) => {
46
+ for ( const [ key , value ] of Object . entries ( node ) ) {
47
+ const fullKey = `${ prefix } .${ key } ` ;
48
+ if ( typeof value === "function" ) {
49
+ this . registry [ fullKey ] = value as QueryKeyBuilder < any , any > ;
50
+ } else if ( value && typeof value === "object" ) {
51
+ register ( fullKey , value ) ;
52
+ }
53
+ }
54
+ } ;
55
+
56
+ register ( name , keyMap ) ;
47
57
48
58
return keyMap ;
49
59
}
50
60
51
- /**
52
- * Retrieves all registered query key builders
53
- *
54
- * @returns Readonly registry object with all query key builders
55
- */
56
61
static getQueryKeys ( ) : Readonly < QueryKeyRegistry > {
57
62
return Object . freeze ( { ...this . registry } ) ;
58
63
}
59
64
60
- /**
61
- * Clears all registered keys (primarily for testing)
62
- */
63
65
static clearRegistry ( ) {
64
66
this . registry = { } ;
65
67
this . keyNames . clear ( ) ;
66
68
}
67
69
68
- // Private constructor to prevent instantiation
69
70
private constructor ( ) { }
70
71
}
71
72
@@ -76,11 +77,8 @@ export function migrateLegacyKeys<T extends (...args: any[]) => QueryKey>(
76
77
if ( process . env . NODE_ENV !== "production" ) {
77
78
console . warn ( `Migrating from legacy key: ${ legacyKey } ` ) ;
78
79
}
79
-
80
- // Runtime check to prevent duplicate registration
81
80
if ( QueryKeyManager . getQueryKeys ( ) [ legacyKey ] ) {
82
81
throw new Error ( `Legacy key ${ legacyKey } conflicts with new keys` ) ;
83
82
}
84
-
85
83
return newBuilder ;
86
84
}
0 commit comments