Skip to content

Commit 423d9f7

Browse files
committed
fix(types): stricter meta with required fields
1 parent accea8e commit 423d9f7

File tree

10 files changed

+225
-134
lines changed

10 files changed

+225
-134
lines changed

packages/router/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export type {
5959
RouteRecordMultipleViewsWithChildren,
6060
RouteRecordRedirect,
6161
RouteMeta,
62+
_RouteMetaBase,
6263
RouteComponent,
6364
// RawRouteComponent,
6465
RouteParamsGeneric,

packages/router/src/types/index.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,9 @@ export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
189189
/**
190190
* Internal type for common properties among all kind of {@link RouteRecordRaw}.
191191
*/
192-
export interface _RouteRecordBase extends PathParserOptions {
192+
export interface _RouteRecordBase
193+
extends PathParserOptions,
194+
_RouteRecordBaseMeta {
193195
/**
194196
* Path of the record. Should start with `/` unless the record is the child of
195197
* another record.
@@ -228,7 +230,7 @@ export interface _RouteRecordBase extends PathParserOptions {
228230
/**
229231
* Arbitrary data attached to the record.
230232
*/
231-
meta?: RouteMeta
233+
// meta?: RouteMeta
232234

233235
/**
234236
* Array of nested routes.
@@ -241,6 +243,12 @@ export interface _RouteRecordBase extends PathParserOptions {
241243
props?: _RouteRecordProps | Record<string, _RouteRecordProps>
242244
}
243245

246+
/**
247+
* Default type for RouteMeta when not augmented.
248+
* @internal
249+
*/
250+
export type _RouteMetaBase = Record<string | number | symbol, unknown>
251+
244252
/**
245253
* Interface to type `meta` fields in route records.
246254
*
@@ -257,7 +265,33 @@ export interface _RouteRecordBase extends PathParserOptions {
257265
* }
258266
* ```
259267
*/
260-
export interface RouteMeta extends Record<string | number | symbol, unknown> {}
268+
export interface RouteMeta extends _RouteMetaBase {}
269+
270+
/**
271+
* Returns `true` if the passed `RouteMeta` type hasn't been augmented. Return `false` otherwise.
272+
* @internal
273+
*/
274+
export type IsRouteMetaBase<RM> = _RouteMetaBase extends RM ? true : false
275+
/**
276+
* Returns `true` if the passed `RouteMeta` type has been augmented with required fields. Return `false` otherwise.
277+
* @internal
278+
*/
279+
export type IsRouteMetaRequired<RM> = Partial<RM> extends RM ? false : true
280+
281+
export type _RouteRecordBaseMeta = IsRouteMetaRequired<RouteMeta> extends true
282+
? {
283+
/**
284+
* Arbitrary data attached to the record. Required because the `RouteMeta` type has been augmented with required
285+
* fields.
286+
*/
287+
meta: RouteMeta
288+
}
289+
: {
290+
/**
291+
* Arbitrary data attached to the record.
292+
*/
293+
meta?: RouteMeta
294+
}
261295

262296
/**
263297
* Route Record defining one single component with the `component` option.

packages/router/test-dts/components.test-d.tsx

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,32 @@ import {
55
createRouter,
66
createMemoryHistory,
77
} from './index'
8-
import { expectTypeOf } from 'vitest'
8+
import { it, describe, expectTypeOf } from 'vitest'
99

10-
let router = createRouter({
11-
history: createMemoryHistory(),
12-
routes: [],
13-
})
10+
describe('Components', () => {
11+
let router = createRouter({
12+
history: createMemoryHistory(),
13+
routes: [],
14+
})
1415

15-
// RouterLink
16-
// @ts-expect-error missing to
17-
expectError(<RouterLink />)
18-
// @ts-expect-error: invalid prop
19-
expectError(<RouterLink to="/" custom="text" />)
20-
// @ts-expect-error: invalid prop
21-
expectError(<RouterLink to="/" replace="text" />)
22-
expectTypeOf<JSX.Element>(<RouterLink to="/foo" replace />)
23-
expectTypeOf<JSX.Element>(<RouterLink to="/foo" />)
24-
expectTypeOf<JSX.Element>(<RouterLink class="link" to="/foo" />)
25-
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} />)
26-
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} custom />)
16+
// TODO: split into multiple tests
17+
it('works', () => {
18+
// RouterLink
19+
// @ts-expect-error missing to
20+
expectError(<RouterLink />)
21+
// @ts-expect-error: invalid prop
22+
expectError(<RouterLink to="/" custom="text" />)
23+
// @ts-expect-error: invalid prop
24+
expectError(<RouterLink to="/" replace="text" />)
25+
expectTypeOf<JSX.Element>(<RouterLink to="/foo" replace />)
26+
expectTypeOf<JSX.Element>(<RouterLink to="/foo" />)
27+
expectTypeOf<JSX.Element>(<RouterLink class="link" to="/foo" />)
28+
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} />)
29+
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} custom />)
2730

28-
// RouterView
29-
expectTypeOf<JSX.Element>(<RouterView class="view" />)
30-
expectTypeOf<JSX.Element>(<RouterView name="foo" />)
31-
expectTypeOf<JSX.Element>(<RouterView route={router.currentRoute.value} />)
31+
// RouterView
32+
expectTypeOf<JSX.Element>(<RouterView class="view" />)
33+
expectTypeOf<JSX.Element>(<RouterView name="foo" />)
34+
expectTypeOf<JSX.Element>(<RouterView route={router.currentRoute.value} />)
35+
})
36+
})
Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
1-
import { expectTypeOf } from 'vitest'
2-
import { Router, RouteLocationNormalizedLoaded } from './index'
1+
import { describe, expectTypeOf, it } from 'vitest'
2+
import {
3+
useRouter,
4+
useRoute,
5+
// rename types for better error messages, otherwise they have the same name
6+
// RouteLocationNormalizedLoadedTyped as I_RLNLT
7+
} from './index'
38
import { defineComponent } from 'vue'
49

5-
defineComponent({
6-
methods: {
7-
doStuff() {
8-
expectTypeOf<Router>(this.$router)
9-
expectTypeOf<RouteLocationNormalizedLoaded>(this.$route)
10-
},
11-
},
10+
describe('Instance types', () => {
11+
it('creates a $route instance property', () => {
12+
defineComponent({
13+
methods: {
14+
doStuff() {
15+
// TODO: can't do a proper check because of typed routes
16+
expectTypeOf(this.$route.params).toMatchTypeOf(useRoute().params)
17+
},
18+
},
19+
})
20+
})
21+
22+
it('creates $router instance properties', () => {
23+
defineComponent({
24+
methods: {
25+
doStuff() {
26+
// TODO: can't do a proper check because of typed routes
27+
expectTypeOf(this.$router.back).toEqualTypeOf(useRouter().back)
28+
},
29+
},
30+
})
31+
})
1232
})

packages/router/test-dts/meta.test-d.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { describe, it, expectTypeOf } from 'vitest'
44

55
const component = defineComponent({})
66

7-
declare module './index' {
7+
declare module '.' {
88
interface RouteMeta {
99
requiresAuth?: boolean
10-
nested: { foo: string }
10+
// TODO: it would be nice to be able to test required meta without polluting all tests
11+
nested?: { foo: string }
1112
}
1213
}
1314

@@ -27,14 +28,18 @@ describe('RouteMeta', () => {
2728
},
2829
},
2930
},
30-
{
31-
path: '/foo',
32-
component,
33-
// @ts-expect-error
34-
meta: {},
35-
},
3631
],
3732
})
33+
34+
router.addRoute({
35+
path: '/foo',
36+
component,
37+
meta: {
38+
nested: {
39+
foo: 'foo',
40+
},
41+
},
42+
})
3843
})
3944

4045
it('route location in guards', () => {
@@ -43,9 +48,12 @@ describe('RouteMeta', () => {
4348
routes: [],
4449
})
4550
router.beforeEach(to => {
46-
expectTypeOf<{ requiresAuth?: Boolean; nested: { foo: string } }>(to.meta)
51+
expectTypeOf<{ requiresAuth?: Boolean; nested?: { foo: string } }>(
52+
to.meta
53+
)
4754
expectTypeOf<unknown>(to.meta.lol)
48-
if (to.meta.nested.foo == 'foo' || to.meta.lol) return false
55+
if (to.meta.nested?.foo == 'foo' || to.meta.lol) return false
56+
return
4957
})
5058
})
5159
})
Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expectTypeOf } from 'vitest'
1+
import { expectTypeOf, describe, it } from 'vitest'
22
import {
33
createRouter,
44
createWebHistory,
@@ -14,44 +14,49 @@ const router = createRouter({
1414
routes: [],
1515
})
1616

17-
router.beforeEach((to, from) => {
18-
return { path: '/' }
19-
})
20-
21-
router.beforeEach((to, from) => {
22-
return '/'
23-
})
24-
25-
router.beforeEach((to, from) => {
26-
return false
27-
})
28-
29-
router.beforeEach((to, from, next) => {
30-
next(undefined)
31-
})
32-
33-
// @ts-expect-error
34-
router.beforeEach((to, from, next) => {
35-
return Symbol('not supported')
36-
})
37-
// @ts-expect-error
38-
router.beforeEach(() => {
39-
return Symbol('not supported')
40-
})
41-
42-
router.beforeEach((to, from, next) => {
43-
// @ts-expect-error
44-
next(Symbol('not supported'))
45-
})
46-
47-
router.afterEach((to, from, failure) => {
48-
expectTypeOf<NavigationFailure | undefined | void>(failure)
49-
if (isNavigationFailure(failure)) {
50-
expectTypeOf<RouteLocationNormalized>(failure.from)
51-
expectTypeOf<RouteLocationRaw>(failure.to)
52-
}
53-
if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
54-
expectTypeOf<RouteLocationNormalized>(failure.from)
55-
expectTypeOf<RouteLocationRaw>(failure.to)
56-
}
17+
describe('Navigation guards', () => {
18+
// TODO: split into multiple tests
19+
it('works', () => {
20+
router.beforeEach((to, from) => {
21+
return { path: '/' }
22+
})
23+
24+
router.beforeEach((to, from) => {
25+
return '/'
26+
})
27+
28+
router.beforeEach((to, from) => {
29+
return false
30+
})
31+
32+
router.beforeEach((to, from, next) => {
33+
next(undefined)
34+
})
35+
36+
// @ts-expect-error
37+
router.beforeEach((to, from, next) => {
38+
return Symbol('not supported')
39+
})
40+
// @ts-expect-error
41+
router.beforeEach(() => {
42+
return Symbol('not supported')
43+
})
44+
45+
router.beforeEach((to, from, next) => {
46+
// @ts-expect-error
47+
next(Symbol('not supported'))
48+
})
49+
50+
router.afterEach((to, from, failure) => {
51+
expectTypeOf<NavigationFailure | undefined | void>(failure)
52+
if (isNavigationFailure(failure)) {
53+
expectTypeOf<RouteLocationNormalized>(failure.from)
54+
expectTypeOf<RouteLocationRaw>(failure.to)
55+
}
56+
if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
57+
expectTypeOf<RouteLocationNormalized>(failure.from)
58+
expectTypeOf<RouteLocationRaw>(failure.to)
59+
}
60+
})
61+
})
5762
})

0 commit comments

Comments
 (0)