Skip to content

Commit 0d5c25c

Browse files
committed
refactor: add Error inference (will be removed)
As noted in the docs of this commit, the error can come from unexpected places. Therefore, it's not correct to set its type. In the end, we will have to check for the error type in a component anyway, so it's better not to have the error type inference. It will be removed in the next commit.
1 parent f81e6f4 commit 0d5c25c

File tree

6 files changed

+180
-37
lines changed

6 files changed

+180
-37
lines changed

src/data-loaders/createDataLoader.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { type _PromiseMerged } from './utils'
44
import { type NavigationResult } from './navigation-guard'
55
import type { RouteLocationNormalizedLoaded, Router } from 'vue-router'
66
import { type _Awaitable } from '../utils'
7+
import { ErrorDefault } from './types-config'
78

89
/**
910
* Base type for a data loader entry. Each Data Loader has its own entry in the `loaderEntries` (accessible via `[LOADER_ENTRIES_KEY]`) map.
1011
*/
11-
export interface DataLoaderEntryBase<Data = unknown> {
12+
export interface DataLoaderEntryBase<Data = unknown, TError = unknown> {
1213
/**
1314
* Data stored in the entry.
1415
*/
@@ -17,7 +18,7 @@ export interface DataLoaderEntryBase<Data = unknown> {
1718
/**
1819
* Error if there was an error.
1920
*/
20-
error: ShallowRef<any> // any is simply more convenient for errors
21+
error: ShallowRef<TError | null> // any is simply more convenient for errors
2122

2223
/**
2324
* Location the data was loaded for or `null` if the data is not loaded.
@@ -56,7 +57,7 @@ export interface DataLoaderEntryBase<Data = unknown> {
5657
* Error that was staged by a loader. This is used to avoid showing the old error while the new data is loading.
5758
* Calling the internal `commit()` function will replace the error with the staged error.
5859
*/
59-
stagedError: any | null
60+
stagedError: TError | null
6061

6162
// entry instance
6263

@@ -127,7 +128,7 @@ export interface _DefineDataLoaderOptionsBase_Common {
127128
* Options for a data loader that returns a data that is possibly `undefined`. Available for data loaders
128129
* implementations so they can be used in `defineLoader()` overloads.
129130
*/
130-
export interface DefineDataLoaderOptionsBase_LaxData
131+
export interface DefineDataLoaderOptionsBase_LaxData<TError = any>
131132
extends _DefineDataLoaderOptionsBase_Common {
132133
lazy?:
133134
| boolean
@@ -142,10 +143,36 @@ export interface DefineDataLoaderOptionsBase_LaxData
142143

143144
errors?:
144145
| boolean
145-
| Array<new (...args: any) => any>
146-
| ((reason?: unknown) => 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)
147152
}
148153

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+
149176
/**
150177
* Options for a data loader making the data defined without it being possibly `undefined`. Available for data loaders
151178
* implementations so they can be used in `defineLoader()` overloads.
@@ -192,7 +219,7 @@ export interface DefineDataLoader<Context extends DataLoaderContextBase> {
192219
* Data Loader composable returned by `defineLoader()`.
193220
* @see {@link DefineDataLoader}
194221
*/
195-
export interface UseDataLoader<Data = unknown> {
222+
export interface UseDataLoader<Data = unknown, TError = unknown> {
196223
[IS_USE_DATA_LOADER_KEY]: true
197224

198225
/**
@@ -222,7 +249,7 @@ export interface UseDataLoader<Data = unknown> {
222249
// excluding `undefined` allows to await for lazy loaders and others
223250
Exclude<Data, NavigationResult | undefined>,
224251
// or use it as a composable
225-
UseDataLoaderResult<Exclude<Data, NavigationResult>>
252+
UseDataLoaderResult<Exclude<Data, NavigationResult>, TError>
226253
>
227254

228255
/**
@@ -236,7 +263,7 @@ export interface UseDataLoader<Data = unknown> {
236263
* Internal properties of a data loader composable. Used by the internal implementation of `defineLoader()`. **Should
237264
* not be used in application code.**
238265
*/
239-
export interface UseDataLoaderInternals<Data = unknown> {
266+
export interface UseDataLoaderInternals<Data = unknown, TError = unknown> {
240267
/**
241268
* Loads the data from the cache if possible, otherwise loads it from the loader and awaits it.
242269
*
@@ -263,13 +290,13 @@ export interface UseDataLoaderInternals<Data = unknown> {
263290
*
264291
* @param router - router instance
265292
*/
266-
getEntry(router: Router): DataLoaderEntryBase<Data>
293+
getEntry(router: Router): DataLoaderEntryBase<Data, TError>
267294
}
268295

269296
/**
270297
* Return value of a loader composable defined with `defineLoader()`.
271298
*/
272-
export interface UseDataLoaderResult<Data = unknown> {
299+
export interface UseDataLoaderResult<Data = unknown, TError = ErrorDefault> {
273300
/**
274301
* Data returned by the loader. If the data loader is lazy, it will be undefined until the first load.
275302
*/
@@ -283,7 +310,7 @@ export interface UseDataLoaderResult<Data = unknown> {
283310
/**
284311
* Error if there was an error.
285312
*/
286-
error: ShallowRef<any> // any is simply more convenient for errors
313+
error: ShallowRef<TError | null> // any is simply more convenient for errors
287314

288315
/**
289316
* 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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,21 @@ describe('defineBasicLoader', () => {
107107
defineColadaLoader({ key, query, errors: [] })().data.value
108108
).toEqualTypeOf<UserData | undefined>()
109109
expectTypeOf(
110-
defineColadaLoader({ key, query, errors: () => true })().data.value
110+
defineColadaLoader({ key, query, errors: (e) => e instanceof Error })().data.value
111111
).toEqualTypeOf<UserData | undefined>()
112+
expectTypeOf(
113+
defineColadaLoader({ key, query, errors: (e) => typeof e == 'object' && e != null })().data.value
114+
).toEqualTypeOf<UserData | undefined>()
115+
})
116+
117+
it('can type the error with a type guard', () => {
118+
expectTypeOf(
119+
defineColadaLoader({
120+
key,
121+
query,
122+
errors: (e): e is Error => e instanceof Error,
123+
})().error.value
124+
).toEqualTypeOf<Error | null>()
112125
})
113126

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

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

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ 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'
56

67
describe('defineBasicLoader', () => {
78
interface UserData {
@@ -95,11 +96,88 @@ describe('defineBasicLoader', () => {
9596
expectTypeOf(
9697
defineBasicLoader(loaderUser, { errors: [] })().data.value
9798
).toEqualTypeOf<UserData | undefined>()
99+
expectTypeOf(
100+
defineBasicLoader(loaderUser, { errors: (e) => e instanceof Error })()
101+
.data.value
102+
).toEqualTypeOf<UserData | undefined>()
98103
expectTypeOf(
99104
defineBasicLoader(loaderUser, { errors: () => true })().data.value
100105
).toEqualTypeOf<UserData | undefined>()
101106
})
102107

108+
class MyError1 extends Error {
109+
// properties to differentiate the errors types
110+
p1: string = 'p1'
111+
}
112+
class MyError2 extends Error {
113+
p2: string = 'p2'
114+
}
115+
116+
it('can type the error with a type guard', () => {
117+
expectTypeOf(
118+
defineBasicLoader(loaderUser, {
119+
errors: (e) => e instanceof Error,
120+
})().error.value
121+
).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>()
179+
})
180+
103181
it('makes data possibly undefined when server is not true', () => {
104182
expectTypeOf(
105183
defineBasicLoader(loaderUser, { server: false })().data.value

0 commit comments

Comments
 (0)