Skip to content

Commit

Permalink
fix(types): stricter meta with required fields
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jul 1, 2024
1 parent accea8e commit 423d9f7
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 134 deletions.
1 change: 1 addition & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type {
RouteRecordMultipleViewsWithChildren,
RouteRecordRedirect,
RouteMeta,
_RouteMetaBase,
RouteComponent,
// RawRouteComponent,
RouteParamsGeneric,
Expand Down
40 changes: 37 additions & 3 deletions packages/router/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,9 @@ export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
/**
* Internal type for common properties among all kind of {@link RouteRecordRaw}.
*/
export interface _RouteRecordBase extends PathParserOptions {
export interface _RouteRecordBase
extends PathParserOptions,
_RouteRecordBaseMeta {
/**
* Path of the record. Should start with `/` unless the record is the child of
* another record.
Expand Down Expand Up @@ -228,7 +230,7 @@ export interface _RouteRecordBase extends PathParserOptions {
/**
* Arbitrary data attached to the record.
*/
meta?: RouteMeta
// meta?: RouteMeta

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

/**
* Default type for RouteMeta when not augmented.
* @internal
*/
export type _RouteMetaBase = Record<string | number | symbol, unknown>

/**
* Interface to type `meta` fields in route records.
*
Expand All @@ -257,7 +265,33 @@ export interface _RouteRecordBase extends PathParserOptions {
* }
* ```
*/
export interface RouteMeta extends Record<string | number | symbol, unknown> {}
export interface RouteMeta extends _RouteMetaBase {}

/**
* Returns `true` if the passed `RouteMeta` type hasn't been augmented. Return `false` otherwise.
* @internal
*/
export type IsRouteMetaBase<RM> = _RouteMetaBase extends RM ? true : false
/**
* Returns `true` if the passed `RouteMeta` type has been augmented with required fields. Return `false` otherwise.
* @internal
*/
export type IsRouteMetaRequired<RM> = Partial<RM> extends RM ? false : true

export type _RouteRecordBaseMeta = IsRouteMetaRequired<RouteMeta> extends true
? {
/**
* Arbitrary data attached to the record. Required because the `RouteMeta` type has been augmented with required
* fields.
*/
meta: RouteMeta
}
: {
/**
* Arbitrary data attached to the record.
*/
meta?: RouteMeta
}

/**
* Route Record defining one single component with the `component` option.
Expand Down
47 changes: 26 additions & 21 deletions packages/router/test-dts/components.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,32 @@ import {
createRouter,
createMemoryHistory,
} from './index'
import { expectTypeOf } from 'vitest'
import { it, describe, expectTypeOf } from 'vitest'

let router = createRouter({
history: createMemoryHistory(),
routes: [],
})
describe('Components', () => {
let router = createRouter({
history: createMemoryHistory(),
routes: [],
})

// RouterLink
// @ts-expect-error missing to
expectError(<RouterLink />)
// @ts-expect-error: invalid prop
expectError(<RouterLink to="/" custom="text" />)
// @ts-expect-error: invalid prop
expectError(<RouterLink to="/" replace="text" />)
expectTypeOf<JSX.Element>(<RouterLink to="/foo" replace />)
expectTypeOf<JSX.Element>(<RouterLink to="/foo" />)
expectTypeOf<JSX.Element>(<RouterLink class="link" to="/foo" />)
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} />)
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} custom />)
// TODO: split into multiple tests
it('works', () => {
// RouterLink
// @ts-expect-error missing to
expectError(<RouterLink />)
// @ts-expect-error: invalid prop
expectError(<RouterLink to="/" custom="text" />)
// @ts-expect-error: invalid prop
expectError(<RouterLink to="/" replace="text" />)
expectTypeOf<JSX.Element>(<RouterLink to="/foo" replace />)
expectTypeOf<JSX.Element>(<RouterLink to="/foo" />)
expectTypeOf<JSX.Element>(<RouterLink class="link" to="/foo" />)
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} />)
expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} custom />)

// RouterView
expectTypeOf<JSX.Element>(<RouterView class="view" />)
expectTypeOf<JSX.Element>(<RouterView name="foo" />)
expectTypeOf<JSX.Element>(<RouterView route={router.currentRoute.value} />)
// RouterView
expectTypeOf<JSX.Element>(<RouterView class="view" />)
expectTypeOf<JSX.Element>(<RouterView name="foo" />)
expectTypeOf<JSX.Element>(<RouterView route={router.currentRoute.value} />)
})
})
38 changes: 29 additions & 9 deletions packages/router/test-dts/legacy.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import { expectTypeOf } from 'vitest'
import { Router, RouteLocationNormalizedLoaded } from './index'
import { describe, expectTypeOf, it } from 'vitest'
import {
useRouter,
useRoute,
// rename types for better error messages, otherwise they have the same name
// RouteLocationNormalizedLoadedTyped as I_RLNLT
} from './index'
import { defineComponent } from 'vue'

defineComponent({
methods: {
doStuff() {
expectTypeOf<Router>(this.$router)
expectTypeOf<RouteLocationNormalizedLoaded>(this.$route)
},
},
describe('Instance types', () => {
it('creates a $route instance property', () => {
defineComponent({
methods: {
doStuff() {
// TODO: can't do a proper check because of typed routes
expectTypeOf(this.$route.params).toMatchTypeOf(useRoute().params)
},
},
})
})

it('creates $router instance properties', () => {
defineComponent({
methods: {
doStuff() {
// TODO: can't do a proper check because of typed routes
expectTypeOf(this.$router.back).toEqualTypeOf(useRouter().back)
},
},
})
})
})
28 changes: 18 additions & 10 deletions packages/router/test-dts/meta.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { describe, it, expectTypeOf } from 'vitest'

const component = defineComponent({})

declare module './index' {
declare module '.' {
interface RouteMeta {
requiresAuth?: boolean
nested: { foo: string }
// TODO: it would be nice to be able to test required meta without polluting all tests
nested?: { foo: string }
}
}

Expand All @@ -27,14 +28,18 @@ describe('RouteMeta', () => {
},
},
},
{
path: '/foo',
component,
// @ts-expect-error
meta: {},
},
],
})

router.addRoute({
path: '/foo',
component,
meta: {
nested: {
foo: 'foo',
},
},
})
})

it('route location in guards', () => {
Expand All @@ -43,9 +48,12 @@ describe('RouteMeta', () => {
routes: [],
})
router.beforeEach(to => {
expectTypeOf<{ requiresAuth?: Boolean; nested: { foo: string } }>(to.meta)
expectTypeOf<{ requiresAuth?: Boolean; nested?: { foo: string } }>(
to.meta
)
expectTypeOf<unknown>(to.meta.lol)
if (to.meta.nested.foo == 'foo' || to.meta.lol) return false
if (to.meta.nested?.foo == 'foo' || to.meta.lol) return false
return
})
})
})
87 changes: 46 additions & 41 deletions packages/router/test-dts/navigationGuards.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expectTypeOf } from 'vitest'
import { expectTypeOf, describe, it } from 'vitest'
import {
createRouter,
createWebHistory,
Expand All @@ -14,44 +14,49 @@ const router = createRouter({
routes: [],
})

router.beforeEach((to, from) => {
return { path: '/' }
})

router.beforeEach((to, from) => {
return '/'
})

router.beforeEach((to, from) => {
return false
})

router.beforeEach((to, from, next) => {
next(undefined)
})

// @ts-expect-error
router.beforeEach((to, from, next) => {
return Symbol('not supported')
})
// @ts-expect-error
router.beforeEach(() => {
return Symbol('not supported')
})

router.beforeEach((to, from, next) => {
// @ts-expect-error
next(Symbol('not supported'))
})

router.afterEach((to, from, failure) => {
expectTypeOf<NavigationFailure | undefined | void>(failure)
if (isNavigationFailure(failure)) {
expectTypeOf<RouteLocationNormalized>(failure.from)
expectTypeOf<RouteLocationRaw>(failure.to)
}
if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
expectTypeOf<RouteLocationNormalized>(failure.from)
expectTypeOf<RouteLocationRaw>(failure.to)
}
describe('Navigation guards', () => {
// TODO: split into multiple tests
it('works', () => {
router.beforeEach((to, from) => {
return { path: '/' }
})

router.beforeEach((to, from) => {
return '/'
})

router.beforeEach((to, from) => {
return false
})

router.beforeEach((to, from, next) => {
next(undefined)
})

// @ts-expect-error
router.beforeEach((to, from, next) => {
return Symbol('not supported')
})
// @ts-expect-error
router.beforeEach(() => {
return Symbol('not supported')
})

router.beforeEach((to, from, next) => {
// @ts-expect-error
next(Symbol('not supported'))
})

router.afterEach((to, from, failure) => {
expectTypeOf<NavigationFailure | undefined | void>(failure)
if (isNavigationFailure(failure)) {
expectTypeOf<RouteLocationNormalized>(failure.from)
expectTypeOf<RouteLocationRaw>(failure.to)
}
if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
expectTypeOf<RouteLocationNormalized>(failure.from)
expectTypeOf<RouteLocationRaw>(failure.to)
}
})
})
})
Loading

0 comments on commit 423d9f7

Please sign in to comment.