Skip to content

Commit 5ec4076

Browse files
committed
feat(data-loaders): allow default type for errors
BREAKING CHANGE: The default type for `error` is now `Error`.
1 parent 0d5c25c commit 5ec4076

File tree

8 files changed

+91
-131
lines changed

8 files changed

+91
-131
lines changed

docs/data-loaders/error-handling.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,29 @@ When you use both, global and local error handling, the local error handling has
148148
- if local `errors` is `false`: abort the navigation -> `data` is not `undefined`
149149
- if local `errors` is `true`: rely on the globally defined `errors` option -> `data` is possibly `undefined`
150150
- else: rely on the local `errors` option -> `data` is possibly `undefined`
151+
152+
## TypeScript
153+
154+
You will notice that the type of `error` is `Error | null` even when you specify the `errors` option. This is because if we call the `reload()` method (meaning we are outside of a navigation), the error isn't discarded, it appears in the `error` property **without being filtered** by the `errors` option.
155+
156+
In practice, depending on how you handle the error, you will add a [type guard](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) inside the component responsible for displaying an error or directly in a `v-if` in the template.
157+
158+
```vue-html
159+
<template>
160+
<!-- ... -->
161+
<p v-if="isMyError(error)">{{ error.message }}</p>
162+
</template>
163+
```
164+
165+
If you want to be even stricter, you can override the default `Error` type with `unknown` (or anything else) by augmenting the `TypesConfig` interface.
166+
167+
```ts
168+
// types-extension.d.ts
169+
import 'unplugin-vue-router/data-loaders'
170+
export {}
171+
declare module 'unplugin-vue-router/data-loaders' {
172+
interface TypesConfig {
173+
Error: unknown
174+
}
175+
}
176+
```

src/data-loaders/createDataLoader.ts

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface DataLoaderEntryBase<Data = unknown, TError = unknown> {
1818
/**
1919
* Error if there was an error.
2020
*/
21-
error: ShallowRef<TError | null> // any is simply more convenient for errors
21+
error: ShallowRef<TError | null>
2222

2323
/**
2424
* Location the data was loaded for or `null` if the data is not loaded.
@@ -128,7 +128,7 @@ export interface _DefineDataLoaderOptionsBase_Common {
128128
* Options for a data loader that returns a data that is possibly `undefined`. Available for data loaders
129129
* implementations so they can be used in `defineLoader()` overloads.
130130
*/
131-
export interface DefineDataLoaderOptionsBase_LaxData<TError = any>
131+
export interface DefineDataLoaderOptionsBase_LaxData
132132
extends _DefineDataLoaderOptionsBase_Common {
133133
lazy?:
134134
| boolean
@@ -143,36 +143,12 @@ export interface DefineDataLoaderOptionsBase_LaxData<TError = any>
143143

144144
errors?:
145145
| boolean
146-
// NOTE:
147-
// | (any extends TError ? any[] : readonly (new (...args: any[]) => TError)[])
148-
| Array<new (...args: any[]) => TError>
149-
| (any extends TError
150-
? (reason?: unknown) => boolean
151-
: (reason?: unknown) => reason is TError)
146+
// array of constructors
147+
| Array<new (...args: any[]) => any>
148+
// custom type guard
149+
| ((reason?: unknown) => boolean)
152150
}
153151

154-
export function errorsFromArray<
155-
const T extends readonly (new (...args: any) => any)[],
156-
>(
157-
errorConstructorsArray: T
158-
): (reason?: unknown) => reason is _UnionFromConstructorsArray<T> {
159-
return (r: unknown): r is _UnionFromConstructorsArray<T> =>
160-
errorConstructorsArray.some((ErrConstructor) => r instanceof ErrConstructor)
161-
}
162-
163-
/**
164-
* Extracts the union of the constructors from an array of constructors.
165-
* @internal
166-
*/
167-
export type _UnionFromConstructorsArray<T extends readonly any[]> = T extends readonly [
168-
new (...args: any[]) => infer R,
169-
...infer Rest,
170-
]
171-
? Rest extends readonly [any, ...any[]]
172-
? R | _UnionFromConstructorsArray<Rest>
173-
: R
174-
: never
175-
176152
/**
177153
* Options for a data loader making the data defined without it being possibly `undefined`. Available for data loaders
178154
* implementations so they can be used in `defineLoader()` overloads.
@@ -256,7 +232,7 @@ export interface UseDataLoader<Data = unknown, TError = unknown> {
256232
* Internals of the data loader.
257233
* @internal
258234
*/
259-
_: UseDataLoaderInternals<Exclude<Data, NavigationResult | undefined>>
235+
_: UseDataLoaderInternals<Exclude<Data, NavigationResult | undefined>, TError>
260236
}
261237

262238
/**
@@ -310,7 +286,7 @@ export interface UseDataLoaderResult<Data = unknown, TError = ErrorDefault> {
310286
/**
311287
* Error if there was an error.
312288
*/
313-
error: ShallowRef<TError | null> // any is simply more convenient for errors
289+
error: ShallowRef<TError | null>
314290

315291
/**
316292
* Reload the data using the current route location. Returns a promise that resolves when the data is reloaded. This

src/data-loaders/defineColadaLoader.test-d.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,23 @@ describe('defineBasicLoader', () => {
107107
defineColadaLoader({ key, query, errors: [] })().data.value
108108
).toEqualTypeOf<UserData | undefined>()
109109
expectTypeOf(
110-
defineColadaLoader({ key, query, errors: (e) => e instanceof Error })().data.value
110+
defineColadaLoader({ key, query, errors: (e) => e instanceof Error })()
111+
.data.value
111112
).toEqualTypeOf<UserData | undefined>()
112113
expectTypeOf(
113-
defineColadaLoader({ key, query, errors: (e) => typeof e == 'object' && e != null })().data.value
114+
defineColadaLoader({
115+
key,
116+
query,
117+
errors: (e) => typeof e == 'object' && e != null,
118+
})().data.value
114119
).toEqualTypeOf<UserData | undefined>()
115120
})
116121

117-
it('can type the error with a type guard', () => {
122+
it('error is typed to Error by default', () => {
118123
expectTypeOf(
119124
defineColadaLoader({
120125
key,
121126
query,
122-
errors: (e): e is Error => e instanceof Error,
123127
})().error.value
124128
).toEqualTypeOf<Error | null>()
125129
})

src/data-loaders/defineColadaLoader.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
type UseDataLoaderResult,
1616
type _DefineLoaderEntryMap,
1717
type _PromiseMerged,
18+
type ErrorDefault,
1819
ABORT_CONTROLLER_KEY,
1920
APP_KEY,
2021
IS_USE_DATA_LOADER_KEY,
@@ -381,7 +382,9 @@ export function defineColadaLoader<Data>(
381382
const entries = router[
382383
LOADER_ENTRIES_KEY
383384
]! as unknown as _DefineLoaderEntryMap<DataLoaderColadaEntry<unknown>>
384-
let entry = entries.get(loader) as DataLoaderColadaEntry<Data> | undefined
385+
let entry = entries.get(loader) as
386+
| DataLoaderColadaEntry<Data, ErrorDefault>
387+
| undefined
385388

386389
if (
387390
// if the entry doesn't exist, create it with load and ensure it's loading
@@ -400,7 +403,7 @@ export function defineColadaLoader<Data>(
400403
)
401404
}
402405

403-
entry = entries.get(loader)! as DataLoaderColadaEntry<Data>
406+
entry = entries.get(loader)! as DataLoaderColadaEntry<Data, ErrorDefault>
404407

405408
// add ourselves to the parent entry children
406409
if (parentEntry) {
@@ -558,7 +561,7 @@ export type DefineDataColadaLoaderOptions<
558561
export interface DataColadaLoaderContext extends DataLoaderContextBase {}
559562

560563
export interface UseDataLoaderColadaResult<Data>
561-
extends UseDataLoaderResult<Data>,
564+
extends UseDataLoaderResult<Data, ErrorDefault>,
562565
Pick<
563566
UseQueryReturn<Data, any>,
564567
'isPending' | 'refetch' | 'refresh' | 'status' | 'asyncStatus' | 'state'
@@ -568,7 +571,7 @@ export interface UseDataLoaderColadaResult<Data>
568571
* Data Loader composable returned by `defineColadaLoader()`.
569572
*/
570573
export interface UseDataLoaderColada_LaxData<Data>
571-
extends UseDataLoader<Data | undefined> {
574+
extends UseDataLoader<Data | undefined, ErrorDefault> {
572575
/**
573576
* Data Loader composable returned by `defineColadaLoader()`.
574577
*
@@ -603,7 +606,7 @@ export interface UseDataLoaderColada_LaxData<Data>
603606
* Data Loader composable returned by `defineColadaLoader()`.
604607
*/
605608
export interface UseDataLoaderColada_DefinedData<Data>
606-
extends UseDataLoader<Data> {
609+
extends UseDataLoader<Data, ErrorDefault> {
607610
/**
608611
* Data Loader composable returned by `defineColadaLoader()`.
609612
*
@@ -634,7 +637,8 @@ export interface UseDataLoaderColada_DefinedData<Data>
634637
>
635638
}
636639

637-
export interface DataLoaderColadaEntry<Data> extends DataLoaderEntryBase<Data> {
640+
export interface DataLoaderColadaEntry<Data, TError = unknown>
641+
extends DataLoaderEntryBase<Data, TError> {
638642
/**
639643
* Reactive route passed to pinia colada so it automatically refetch
640644
*/
@@ -648,7 +652,7 @@ export interface DataLoaderColadaEntry<Data> extends DataLoaderEntryBase<Data> {
648652
/**
649653
* Extended options for pinia colada
650654
*/
651-
ext: UseQueryReturn<Data> | null
655+
ext: UseQueryReturn<Data, TError> | null
652656
}
653657

654658
interface TrackedRoute {

src/data-loaders/defineLoader.test-d.ts

Lines changed: 2 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { describe, it, expectTypeOf } from 'vitest'
22
import { defineBasicLoader } from './defineLoader'
33
import type { Ref } from 'vue'
44
import { NavigationResult } from './navigation-guard'
5-
import { errorsFromArray, type _UnionFromConstructorsArray } from './createDataLoader'
65

76
describe('defineBasicLoader', () => {
87
interface UserData {
@@ -113,69 +112,10 @@ describe('defineBasicLoader', () => {
113112
p2: string = 'p2'
114113
}
115114

116-
it('can type the error with a type guard', () => {
115+
it('uses a default type of Error | null', () => {
117116
expectTypeOf(
118-
defineBasicLoader(loaderUser, {
119-
errors: (e) => e instanceof Error,
120-
})().error.value
117+
defineBasicLoader(loaderUser, {})().error.value
121118
).toEqualTypeOf<Error | null>()
122-
123-
expectTypeOf(
124-
defineBasicLoader(loaderUser, {
125-
errors: (e) => e instanceof MyError1 || e instanceof MyError2,
126-
})().error.value
127-
).toEqualTypeOf<MyError1 | MyError2 | null>()
128-
})
129-
130-
it('errorsFromArray fails on non constructors', () => {
131-
errorsFromArray([
132-
// @ts-expect-error: no constructor
133-
2,
134-
// @ts-expect-error: no constructor
135-
{},
136-
])
137-
})
138-
139-
it('errorsFromArray narrows down types', () => {
140-
let e: unknown
141-
if (errorsFromArray([MyError1])(e)) {
142-
expectTypeOf(e).toEqualTypeOf<MyError1>()
143-
}
144-
})
145-
146-
it('errorsFromArray works with multiple types', () => {
147-
let e: unknown
148-
if (errorsFromArray([MyError1, MyError2])(e)) {
149-
expectTypeOf(e).toEqualTypeOf<MyError1 | MyError2>()
150-
}
151-
})
152-
153-
it('errorsFromArray works with multiple incompatible types', () => {
154-
const e1 = [MyError1] as const
155-
const e2 = [MyError1, MyError2] as const
156-
const e3 = [MyError1, Error] as const
157-
expectTypeOf({} as _UnionFromConstructorsArray<typeof e1>).toEqualTypeOf<MyError1>()
158-
expectTypeOf({} as _UnionFromConstructorsArray<typeof e2>).toEqualTypeOf<MyError1 | MyError2>()
159-
expectTypeOf({} as _UnionFromConstructorsArray<typeof e3>).toEqualTypeOf<MyError1 | Error>()
160-
161-
162-
let e: unknown
163-
if (errorsFromArray([MyError1, Error])(e)) {
164-
expectTypeOf(e).toEqualTypeOf<MyError1 | Error>()
165-
}
166-
})
167-
168-
it('can type the error with an array', () => {
169-
expectTypeOf(
170-
defineBasicLoader(loaderUser, {
171-
errors: errorsFromArray([MyError1, MyError2]),
172-
})().error.value
173-
).toEqualTypeOf<MyError1 | MyError2 | null>()
174-
expectTypeOf(
175-
defineBasicLoader(loaderUser, {
176-
errors: [MyError1, MyError2],
177-
})().error.value
178-
).toEqualTypeOf<MyError1 | MyError2 | null>()
179119
})
180120

181121
it('makes data possibly undefined when server is not true', () => {

0 commit comments

Comments
 (0)