Skip to content

Commit

Permalink
feat: allow typing the error with transformError
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Mar 27, 2024
1 parent 8289cd9 commit fd35f6f
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 14 deletions.
26 changes: 23 additions & 3 deletions src/query-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type InjectionKey, type MaybeRefOrGetter, inject } from 'vue'
import type { EntryNodeKey } from './tree-map'
import type { _ObjectFlat } from './utils'
import type { QueryPluginOptions } from './query-plugin'
import type { ErrorDefault } from './types-extension'

/**
* `true` refetch if data is stale (refresh()), `false` never refetch, 'always' always refetch.
Expand Down Expand Up @@ -75,7 +76,7 @@ export interface UseQueryFnContext {
* }
* ```
*/
export interface UseQueryOptions<TResult = unknown> {
export interface UseQueryOptions<TResult = unknown, TError = ErrorDefault> {
/**
* The key used to identify the query. Array of primitives **without** reactive values or a reactive array or getter.
* It should be treaded as an array of dependencies of your queries, e.g. if you use the `route.params.id` property,
Expand Down Expand Up @@ -112,6 +113,23 @@ export interface UseQueryOptions<TResult = unknown> {

initialData?: () => TResult

/**
* Function to type and ensure the `error` property is always an instance of `TError`.
*
* @param error - error thrown
* @example
* ```ts
* useQuery({
* key: ['user', id],
* query: () => fetchUser(id),
* transformError: (error): MyCustomError | UnexpectedError =>
* // this assumes both `MyCustomError` and a `UnexpectedError` are valid error classes
* error instanceof MyCustomError ? error : new UnexpectedError(error),
* })
* ```
*/
transformError?: (error: unknown) => TError

// TODO: move to a plugin
// TODO: rename to refresh since that's the default? and change 'always' to 'force'?
refetchOnMount?: _RefetchOnControl
Expand All @@ -138,10 +156,12 @@ export const USE_QUERY_DEFAULTS = {
refetchOnWindowFocus: true as _RefetchOnControl,
refetchOnReconnect: true as _RefetchOnControl,
refetchOnMount: true as _RefetchOnControl,
// as any to simplify the typing with generics
transformError: (error: unknown) => error as any,
} satisfies Partial<UseQueryOptions>

export type UseQueryOptionsWithDefaults<TResult = unknown> =
typeof USE_QUERY_DEFAULTS & UseQueryOptions<TResult>
export type UseQueryOptionsWithDefaults<TResult = unknown, TError = ErrorDefault> =
typeof USE_QUERY_DEFAULTS & UseQueryOptions<TResult, TError>

export const USE_QUERY_OPTIONS_KEY: InjectionKey<
typeof USE_QUERY_DEFAULTS &
Expand Down
12 changes: 10 additions & 2 deletions src/query-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { UseQueryReturn } from './use-query'
import type { ErrorDefault } from './types-extension'

export interface QueryPluginOptions
extends Omit<UseQueryOptions, 'key' | 'query' | 'initialData'> {
extends Omit<UseQueryOptions, 'key' | 'query' | 'initialData' | 'transformError'> {
/**
* Executes setup code inside `useQuery()` to add custom behavior to all queries. **Must be synchronous**.
*
Expand All @@ -20,7 +20,7 @@ export interface QueryPluginOptions
setup?: <TResult = unknown, TError = ErrorDefault>(
context: _Simplify<
UseQueryReturn<TResult, TError> & {
options: UseQueryOptionsWithDefaults<TResult>
options: UseQueryOptionsWithDefaults<TResult, TError>
}
>,
) => void | Promise<never>
Expand All @@ -29,6 +29,14 @@ export interface QueryPluginOptions
onSuccess?: () => void
onSettled?: () => void
onError?: () => void

/**
* Function to ensure the `error` property is always an instance of the default global type error. Defaults to the
* identity function.
*
* @param error - error thrown
*/
transformError?: (error: unknown) => ErrorDefault
}

export function QueryPlugin(
Expand Down
4 changes: 2 additions & 2 deletions src/query-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface UseQueryEntry<TResult = unknown, TError = unknown>
/**
* Options used to create the query. They can be undefined during hydration but are needed for fetching. This is why `store.ensureEntry()` sets this property.
*/
options?: UseQueryOptionsWithDefaults<TResult>
options?: UseQueryOptionsWithDefaults<TResult, TError>
}

/**
Expand Down Expand Up @@ -144,7 +144,7 @@ export const useQueryCache = defineStore(QUERY_STORE_ID, () => {

function ensureEntry<TResult = unknown, TError = ErrorDefault>(
keyRaw: UseQueryKey,
options: UseQueryOptionsWithDefaults<TResult>,
options: UseQueryOptionsWithDefaults<TResult, TError>,
): UseQueryEntry<TResult, TError> {
if (process.env.NODE_ENV !== 'production' && keyRaw.length === 0) {
throw new Error(
Expand Down
8 changes: 4 additions & 4 deletions src/use-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('useQuery', () => {

enableAutoUnmount(afterEach)

function mountSimple<TResult = number>(
function mountSimple<TResult = number, TError = Error>(
options: Partial<UseQueryOptions<TResult>> = {},
mountOptions?: GlobalMountOptions,
) {
Expand All @@ -38,7 +38,7 @@ describe('useQuery', () => {
render: () => null,
setup() {
return {
...useQuery<TResult>({
...useQuery<TResult, TError>({
key: ['key'],
...options,
// @ts-expect-error: generic unmatched but types work
Expand Down Expand Up @@ -235,7 +235,7 @@ describe('useQuery', () => {
})

describe('refresh data', () => {
function mountDynamicKey<TResult = { id: number, when: number }>(
function mountDynamicKey<TResult = { id: number, when: number }, TError = Error>(
options: Partial<UseQueryOptions<TResult>> & { initialId?: number } = {},
mountOptions?: GlobalMountOptions,
) {
Expand Down Expand Up @@ -264,7 +264,7 @@ describe('useQuery', () => {
// renders again
await nextTick()
},
...useQuery<TResult>({
...useQuery<TResult, TError>({
key: () => ['data', id.value],
...options,
// @ts-expect-error: generic unmatched but types work
Expand Down
27 changes: 27 additions & 0 deletions src/use-query.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@ it('can uses the global error type', () => {
)
})

it('can type the error with "transformError"', () => {
expectTypeOf<MyCustomError | UnexpectedError | null>(
useQuery({
query: async () => 42,
key: ['foo'],
transformError: (error) => {
return error instanceof MyCustomError ? error : new UnexpectedError(error)
},
}).error.value,
)
})

class MyCustomError extends Error {
constructor(message: string) {
super(message)
this.name = 'MyCustomError'
}
}

class UnexpectedError extends Error {
error: unknown
constructor(error: unknown) {
super()
this.error = error
}
}

declare module './types-extension' {
interface TypesConfig {
Error: { custom: Error }
Expand Down
6 changes: 3 additions & 3 deletions src/use-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export interface UseQueryReturn<TResult = unknown, TError = ErrorDefault>
* @param _options - The options of the query
*/
export function useQuery<TResult, TError = ErrorDefault>(
_options: UseQueryOptions<TResult>,
_options: UseQueryOptions<TResult, TError>,
): UseQueryReturn<TResult, TError> {
const store = useQueryCache()
const USE_QUERY_DEFAULTS = useQueryOptions()
Expand All @@ -53,7 +53,7 @@ export function useQuery<TResult, TError = ErrorDefault>(
const options = {
...USE_QUERY_DEFAULTS,
..._options,
} satisfies UseQueryOptionsWithDefaults<TResult>
} satisfies UseQueryOptionsWithDefaults<TResult, TError>

// TODO:
// allows warning against potentially wrong usage
Expand Down Expand Up @@ -171,7 +171,7 @@ export function useQuery<TResult, TError = ErrorDefault>(

// TODO: createQuery with access to other properties as arguments for advanced (maybe even recommended) usage
function _defineQuery<TResult, TError = ErrorDefault>(
setup: () => UseQueryOptions<TResult>,
setup: () => UseQueryOptions<TResult, TError>,
) {
return () => useQuery<TResult, TError>(setup())
}
Expand Down

0 comments on commit fd35f6f

Please sign in to comment.