Skip to content

Commit

Permalink
fix: do not allow dynamic query objects to have extra properties
Browse files Browse the repository at this point in the history
  • Loading branch information
lukemorales committed Feb 9, 2024
1 parent 287fca3 commit 9b5401c
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-trains-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lukemorales/query-key-factory": patch
---

Improve `mergeQueryKeys` type inference and improve type-safety for dynamic query keys
31 changes: 31 additions & 0 deletions src/create-query-keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,37 @@ describe('createQueryKeys', () => {
});

describe('when the function returns an object', () => {
describe('when the object has extra unintended properties', () => {
it('creates a callback that returns the expected shape without the extra unintended properties', () => {
const sut = createQueryKeys('test', {
// @ts-expect-error prop return is invalid as staleTime is an invalidKey
prop: (value: string) => ({
queryKey: [value],
staleTime: Infinity,
}),
});

const result = sut.prop('value');
expect(result).toEqual({
queryKey: ['test', 'prop', 'value'],
});

expect(sut.prop).toHaveStrictType<
{ _def: readonly ['test', 'prop'] } & ((value: string) => {
queryKey: readonly ['test', 'prop', string];
})
>();

expect({} as inferQueryKeys<typeof sut>).toHaveStrictType<{
_def: readonly ['test'];
prop: {
_def: readonly ['test', 'prop'];
queryKey: readonly ['test', 'prop', string];
};
}>();
});
});

describe('when the object has "queryKey"', () => {
it('creates a callback that returns "queryKey"', () => {
const sut = createQueryKeys('test', {
Expand Down
12 changes: 6 additions & 6 deletions src/create-query-keys.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { assertSchemaKeys, omitPrototype } from './internals';
import type {
AnyQueryFactoryOutputCallback,
AnyQueryKey,
QueryFactorySchema,
QueryKeyFactoryResult,
ValidateFactory,
AnyQueryFactoryOutputCallback,
AnyQueryKey,
} from './create-query-keys.types';
import { assertSchemaKeys, omitPrototype } from './internals';
import { type DefinitionKey } from './types';

export function createQueryKeys<Key extends string>(queryDef: Key): DefinitionKey<[Key]>;
export function createQueryKeys<Key extends string, Schema extends QueryFactorySchema>(
queryDef: Key,
schema: ValidateFactory<Schema>,
): QueryKeyFactoryResult<Key, ValidateFactory<Schema>>;
): QueryKeyFactoryResult<Key, Schema>;
export function createQueryKeys<Key extends string, Schema extends QueryFactorySchema>(
queryDef: Key,
schema?: ValidateFactory<Schema>,
): DefinitionKey<[Key]> | QueryKeyFactoryResult<Key, ValidateFactory<Schema>> {
): DefinitionKey<[Key]> | QueryKeyFactoryResult<Key, Schema> {
const defKey: DefinitionKey<[Key]> = {
_def: [queryDef] as const,
};
Expand Down Expand Up @@ -145,7 +145,7 @@ export function createQueryKeys<Key extends string, Schema extends QueryFactoryS
}, new Map<$FactoryProperty, $Factory[$FactoryProperty]>());
};

const transformedSchema = transformSchema(schema, defKey._def);
const transformedSchema = transformSchema(schema as QueryFactorySchema, defKey._def);

return omitPrototype({
...Object.fromEntries(transformedSchema),
Expand Down
85 changes: 47 additions & 38 deletions src/create-query-keys.types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import type { QueryFunction } from '@tanstack/query-core';

import type { ExtractInternalKeys, InternalKey } from './internals';
import type { KeyTuple, AnyMutableOrReadonlyArray, DefinitionKey } from './types';
import type { AnyMutableOrReadonlyArray, DefinitionKey, KeyTuple } from './types';

export type AnyQueryKey = readonly [string, ...any[]];

type ValidFactoryKey = 'queryKey' | 'queryFn' | 'contextQueries';

export type StrictOptions<T> = T extends any[]
? T
: keyof T extends ValidFactoryKey
? T
: { [K in keyof T]: K extends ValidFactoryKey ? T[K] : never };

type NullableQueryKeyRecord = Record<'queryKey', KeyTuple | null>;

type QueryKeyRecord = Record<'queryKey', KeyTuple>;
Expand Down Expand Up @@ -56,21 +64,22 @@ export type QueryFactorySchema = Record<string, FactoryProperty | DynamicKey>;

type InvalidSchema<Schema extends QueryFactorySchema> = Omit<Schema, InternalKey>;

export type ValidateFactory<Schema extends QueryFactorySchema> = Schema extends {
[P in ExtractInternalKeys<Schema>]: Schema[P];
}
? InvalidSchema<Schema>
: {
[P in keyof Schema]: Schema[P];
};
export type ValidateFactory<Schema extends QueryFactorySchema> =
ExtractInternalKeys<Schema> extends never
? {
[P in keyof Schema]: Schema[P] extends (...args: infer Args) => infer R
? (...args: Args) => StrictOptions<R>
: Schema[P];
}
: InvalidSchema<Schema>;

type ExtractNullableKey<Key extends KeyTuple | null | undefined> = Key extends
| [...infer Value]
| readonly [...infer Value]
? Value
: Key extends null | undefined | unknown
? null
: never;
? null
: never;

type ComposeQueryKey<BaseKey extends AnyMutableOrReadonlyArray, Key> = Key extends KeyTuple
? readonly [...BaseKey, ...Key]
Expand All @@ -97,8 +106,8 @@ type FactoryWithContextualQueriesOutput<
[P in keyof ContextQueries]: ContextQueries[P] extends DynamicKey
? DynamicFactoryOutput<[...ComposedKey, P], ContextQueries[P]>
: ContextQueries[P] extends FactoryProperty
? StaticFactoryOutput<[...ComposedKey, P], ContextQueries[P]>
: never;
? StaticFactoryOutput<[...ComposedKey, P], ContextQueries[P]>
: never;
};
}
: Omit<QueryOptionsStruct<ComposedKey, QueryFunction>, 'queryFn'> &
Expand All @@ -107,8 +116,8 @@ type FactoryWithContextualQueriesOutput<
[P in keyof ContextQueries]: ContextQueries[P] extends DynamicKey
? DynamicFactoryOutput<[...ComposedKey, P], ContextQueries[P]>
: ContextQueries[P] extends FactoryProperty
? StaticFactoryOutput<[...ComposedKey, P], ContextQueries[P]>
: never;
? StaticFactoryOutput<[...ComposedKey, P], ContextQueries[P]>
: never;
};
};

Expand Down Expand Up @@ -144,8 +153,8 @@ type FactoryQueryOptionsWithContextualQueriesOutput<
[P in keyof ContextQueries]: ContextQueries[P] extends DynamicKey
? DynamicFactoryOutput<[...Key, P], ContextQueries[P]>
: ContextQueries[P] extends FactoryProperty
? StaticFactoryOutput<[...Key, P], ContextQueries[P]>
: never;
? StaticFactoryOutput<[...Key, P], ContextQueries[P]>
: never;
};
}
: DefinitionKey<BaseKey> &
Expand All @@ -154,8 +163,8 @@ type FactoryQueryOptionsWithContextualQueriesOutput<
[P in keyof ContextQueries]: ContextQueries[P] extends DynamicKey
? DynamicFactoryOutput<[...Key, P], ContextQueries[P]>
: ContextQueries[P] extends FactoryProperty
? StaticFactoryOutput<[...Key, P], ContextQueries[P]>
: never;
? StaticFactoryOutput<[...Key, P], ContextQueries[P]>
: never;
};
};

Expand All @@ -168,14 +177,14 @@ type DynamicFactoryOutput<
) => Output extends [...infer TupleResult] | readonly [...infer TupleResult]
? Omit<QueryOptionsStruct<[...Keys, ...TupleResult], QueryFunction>, 'queryFn'>
: Output extends DynamicQueryFactoryWithContextualQueriesSchema
? Omit<FactoryQueryOptionsWithContextualQueriesOutput<Keys, Output>, '_def'>
: Output extends DynamicQueryFactorySchema
? Omit<FactoryQueryOptionsOutput<Keys, Output>, '_def'>
: Output extends DynamicKeySchemaWithContextualQueries
? Omit<FactoryWithContextualQueriesOutput<Keys, Output>, '_def'>
: Output extends QueryKeyRecord
? Omit<FactoryQueryKeyRecordOutput<Keys, Output>, '_def'>
: never) &
? Omit<FactoryQueryOptionsWithContextualQueriesOutput<Keys, Output>, '_def'>
: Output extends DynamicQueryFactorySchema
? Omit<FactoryQueryOptionsOutput<Keys, Output>, '_def'>
: Output extends DynamicKeySchemaWithContextualQueries
? Omit<FactoryWithContextualQueriesOutput<Keys, Output>, '_def'>
: Output extends QueryKeyRecord
? Omit<FactoryQueryKeyRecordOutput<Keys, Output>, '_def'>
: never) &
DefinitionKey<Keys>;

export type AnyQueryFactoryOutputCallback = DynamicFactoryOutput<[string, ...any[]], DynamicKey>;
Expand All @@ -186,23 +195,23 @@ export type StaticFactoryOutput<
> = Property extends null
? Omit<QueryOptionsStruct<Keys, QueryFunction>, 'queryFn'>
: Property extends [...infer Result] | readonly [...infer Result]
? DefinitionKey<Keys> & Omit<QueryOptionsStruct<[...Keys, ...Result], QueryFunction>, 'queryFn'>
: Property extends QueryFactoryWithContextualQueriesSchema
? FactoryQueryOptionsWithContextualQueriesOutput<Keys, Property>
: Property extends $QueryFactorySchema
? FactoryQueryOptionsOutput<Keys, Property>
: Property extends KeySchemaWithContextualQueries
? FactoryWithContextualQueriesOutput<Keys, Property>
: Property extends NullableQueryKeyRecord
? FactoryQueryKeyRecordOutput<Keys, Property>
: never;
? DefinitionKey<Keys> & Omit<QueryOptionsStruct<[...Keys, ...Result], QueryFunction>, 'queryFn'>
: Property extends QueryFactoryWithContextualQueriesSchema
? FactoryQueryOptionsWithContextualQueriesOutput<Keys, Property>
: Property extends $QueryFactorySchema
? FactoryQueryOptionsOutput<Keys, Property>
: Property extends KeySchemaWithContextualQueries
? FactoryWithContextualQueriesOutput<Keys, Property>
: Property extends NullableQueryKeyRecord
? FactoryQueryKeyRecordOutput<Keys, Property>
: never;

type FactoryOutput<Key extends string, Schema extends QueryFactorySchema> = DefinitionKey<[Key]> & {
[P in keyof Schema]: Schema[P] extends DynamicKey
? DynamicFactoryOutput<[Key, P], Schema[P]>
: Schema[P] extends FactoryProperty
? StaticFactoryOutput<[Key, P], Schema[P]>
: never;
? StaticFactoryOutput<[Key, P], Schema[P]>
: never;
};

export type QueryKeyFactoryResult<Key extends string, Schema extends QueryFactorySchema> = FactoryOutput<Key, Schema>;
Expand Down

0 comments on commit 9b5401c

Please sign in to comment.