From 34bfc11cc538edcb3559624e4586a63f282a7dd5 Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Mon, 3 Jun 2024 17:04:55 +0200 Subject: [PATCH] feat: wip typed routes --- packages/playground/src/main.ts | 32 +++ packages/router/__tests__/RouterLink.spec.ts | 2 +- packages/router/__tests__/RouterView.spec.ts | 6 +- packages/router/__tests__/errors.spec.ts | 9 +- .../guards/extractComponentsGuards.spec.ts | 3 +- .../__tests__/guards/guardToPromiseFn.spec.ts | 2 +- .../router/__tests__/matcher/resolve.spec.ts | 4 +- packages/router/__tests__/router.spec.ts | 7 +- packages/router/src/config.ts | 10 +- packages/router/src/errors.ts | 8 +- packages/router/src/index.ts | 5 +- packages/router/src/injectionSymbols.ts | 2 +- packages/router/src/location.ts | 30 +++ packages/router/src/matcher/index.ts | 10 +- packages/router/src/navigationGuards.ts | 8 +- packages/router/src/router.ts | 28 ++- packages/router/src/typed-routes/index.ts | 22 ++ packages/router/src/typed-routes/params.ts | 34 +++ .../router/src/typed-routes/route-location.ts | 223 ++++++++++++++++++ packages/router/src/typed-routes/route-map.ts | 39 +++ .../router/src/typed-routes/route-records.ts | 8 + packages/router/src/types/index.ts | 35 +-- packages/router/src/types/typeGuards.ts | 4 +- packages/router/src/types/utils.ts | 7 + packages/router/src/useApi.ts | 7 +- 25 files changed, 458 insertions(+), 87 deletions(-) create mode 100644 packages/router/src/typed-routes/index.ts create mode 100644 packages/router/src/typed-routes/params.ts create mode 100644 packages/router/src/typed-routes/route-location.ts create mode 100644 packages/router/src/typed-routes/route-map.ts create mode 100644 packages/router/src/typed-routes/route-records.ts diff --git a/packages/playground/src/main.ts b/packages/playground/src/main.ts index ce131cb69..103bb410d 100644 --- a/packages/playground/src/main.ts +++ b/packages/playground/src/main.ts @@ -4,6 +4,7 @@ import type { ComponentPublicInstance } from 'vue' import { router, routerHistory } from './router' import { globalState } from './store' import App from './App.vue' +import { useRoute, type ParamValue, type RouteRecordInfo } from 'vue-router' declare global { interface Window { @@ -29,3 +30,34 @@ app.provide('state', globalState) app.use(router) window.vm = app.mount('#app') + +export interface RouteNamedMap { + home: RouteRecordInfo<'home', '/', Record, Record> + '/[name]': RouteRecordInfo< + '/[name]', + '/:name', + { name: ParamValue }, + { name: ParamValue } + > + '/[...path]': RouteRecordInfo< + '/[...path]', + '/:path(.*)', + { path: ParamValue }, + { path: ParamValue } + > +} + +declare module 'vue-router' { + interface TypesConfig { + RouteNamedMap: RouteNamedMap + } +} + +const r = useRoute() + +if (r.name === '/[name]') { + r.params.name.toUpperCase() + // @ts-expect-error: Not existing route +} else if (r.name === 'nope') { + console.log('nope') +} diff --git a/packages/router/__tests__/RouterLink.spec.ts b/packages/router/__tests__/RouterLink.spec.ts index e3d76b685..36fcfe9f0 100644 --- a/packages/router/__tests__/RouterLink.spec.ts +++ b/packages/router/__tests__/RouterLink.spec.ts @@ -3,11 +3,11 @@ */ import { RouterLink, RouterLinkProps } from '../src/RouterLink' import { - START_LOCATION_NORMALIZED, RouteQueryAndHash, MatcherLocationRaw, RouteLocationNormalized, } from '../src/types' +import { START_LOCATION_NORMALIZED } from '../src/location' import { createMemoryHistory, RouterOptions } from '../src' import { createMockedRoute } from './mount' import { defineComponent, PropType } from 'vue' diff --git a/packages/router/__tests__/RouterView.spec.ts b/packages/router/__tests__/RouterView.spec.ts index 54fea992c..8f1a4d1ba 100644 --- a/packages/router/__tests__/RouterView.spec.ts +++ b/packages/router/__tests__/RouterView.spec.ts @@ -3,10 +3,8 @@ */ import { RouterView } from '../src/RouterView' import { components, RouteLocationNormalizedLoose } from './utils' -import { - START_LOCATION_NORMALIZED, - RouteLocationNormalized, -} from '../src/types' +import { RouteLocationNormalized } from '../src/types' +import { START_LOCATION_NORMALIZED } from '../src/location' import { markRaw } from 'vue' import { createMockedRoute } from './mount' import { mockWarn } from 'jest-mock-warn' diff --git a/packages/router/__tests__/errors.spec.ts b/packages/router/__tests__/errors.spec.ts index c7219dc44..c176832eb 100644 --- a/packages/router/__tests__/errors.spec.ts +++ b/packages/router/__tests__/errors.spec.ts @@ -8,14 +8,13 @@ import { ErrorTypes, } from '../src/errors' import { components, tick } from './utils' -import { - RouteRecordRaw, - NavigationGuard, +import { RouteRecordRaw, NavigationGuard } from '../src/types' +import type { RouteLocationRaw, - START_LOCATION_NORMALIZED, RouteLocationNormalized, -} from '../src/types' +} from '../src/typed-routes' import { mockWarn } from 'jest-mock-warn' +import { START_LOCATION_NORMALIZED } from '../src/location' const routes: Readonly[] = [ { path: '/', component: components.Home }, diff --git a/packages/router/__tests__/guards/extractComponentsGuards.spec.ts b/packages/router/__tests__/guards/extractComponentsGuards.spec.ts index 560d9cc1e..a8bfb4d87 100644 --- a/packages/router/__tests__/guards/extractComponentsGuards.spec.ts +++ b/packages/router/__tests__/guards/extractComponentsGuards.spec.ts @@ -1,5 +1,6 @@ import { extractComponentsGuards } from '../../src/navigationGuards' -import { START_LOCATION_NORMALIZED, RouteRecordRaw } from '../../src/types' +import { RouteRecordRaw } from '../../src/types' +import { START_LOCATION_NORMALIZED } from '../../src/location' import { components } from '../utils' import { normalizeRouteRecord } from '../../src/matcher' import { RouteRecordNormalized } from 'src/matcher/types' diff --git a/packages/router/__tests__/guards/guardToPromiseFn.spec.ts b/packages/router/__tests__/guards/guardToPromiseFn.spec.ts index d2b596f98..b95d47c71 100644 --- a/packages/router/__tests__/guards/guardToPromiseFn.spec.ts +++ b/packages/router/__tests__/guards/guardToPromiseFn.spec.ts @@ -1,5 +1,5 @@ import { guardToPromiseFn } from '../../src/navigationGuards' -import { START_LOCATION_NORMALIZED } from '../../src/types' +import { START_LOCATION_NORMALIZED } from '../../src/location' import { ErrorTypes } from '../../src/errors' import { mockWarn } from 'jest-mock-warn' diff --git a/packages/router/__tests__/matcher/resolve.spec.ts b/packages/router/__tests__/matcher/resolve.spec.ts index de00849e1..517c679bb 100644 --- a/packages/router/__tests__/matcher/resolve.spec.ts +++ b/packages/router/__tests__/matcher/resolve.spec.ts @@ -1,6 +1,5 @@ import { createRouterMatcher, normalizeRouteRecord } from '../../src/matcher' import { - START_LOCATION_NORMALIZED, RouteComponent, RouteRecordRaw, MatcherLocationRaw, @@ -9,6 +8,7 @@ import { import { MatcherLocationNormalizedLoose } from '../utils' import { mockWarn } from 'jest-mock-warn' import { defineComponent } from '@vue/runtime-core' +import { START_LOCATION_NORMALIZED } from '../../src/location' const component: RouteComponent = defineComponent({}) @@ -75,7 +75,7 @@ describe('RouterMatcher.resolve', () => { /** * * @param record - Record or records we are testing the matcher against - * @param location - location we want to reolve against + * @param location - location we want to resolve against * @param [start] Optional currentLocation used when resolving * @returns error */ diff --git a/packages/router/__tests__/router.spec.ts b/packages/router/__tests__/router.spec.ts index bd6b186e0..9f61c7aa7 100644 --- a/packages/router/__tests__/router.spec.ts +++ b/packages/router/__tests__/router.spec.ts @@ -7,12 +7,9 @@ import { } from '../src' import { NavigationFailureType } from '../src/errors' import { createDom, components, tick, nextNavigation } from './utils' -import { - RouteRecordRaw, - RouteLocationRaw, - START_LOCATION_NORMALIZED, -} from '../src/types' +import { RouteRecordRaw, RouteLocationRaw } from '../src/types' import { mockWarn } from 'jest-mock-warn' +import { START_LOCATION_NORMALIZED } from '../src/location' declare var __DEV__: boolean diff --git a/packages/router/src/config.ts b/packages/router/src/config.ts index 1da3f7f15..02204e9ba 100644 --- a/packages/router/src/config.ts +++ b/packages/router/src/config.ts @@ -1,5 +1,13 @@ /** - * Allows customizing existing types of the router that are used globally like `$router`, ``, and `beforeRouteLeave()`. **ONLY FOR INTERNAL USAGE**. + * Allows customizing existing types of the router that are used globally like `$router`, ``, etc. **ONLY FOR INTERNAL USAGE**. + * + * - `$router` - the router instance + * - `$route` - the current route location + * - `beforeRouteEnter` - Page component option + * - `beforeRouteUpdate` - Page component option + * - `beforeRouteLeave` - Page component option + * - `RouterLink` - RouterLink Component + * - `RouterView` - RouterView Component * * @internal */ diff --git a/packages/router/src/errors.ts b/packages/router/src/errors.ts index 021e7a8bb..877a0de21 100644 --- a/packages/router/src/errors.ts +++ b/packages/router/src/errors.ts @@ -1,9 +1,5 @@ -import { - MatcherLocationRaw, - MatcherLocation, - RouteLocationRaw, - RouteLocationNormalized, -} from './types' +import type { MatcherLocationRaw, MatcherLocation } from './types' +import type { RouteLocationRaw, RouteLocationNormalized } from './typed-routes' import { assign } from './utils' /** diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 816dd2c51..140a73183 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -4,6 +4,8 @@ export { createWebHashHistory } from './history/hash' export { createRouterMatcher } from './matcher' export type { RouterMatcher } from './matcher' +export type * from './typed-routes' + export { parseQuery, stringifyQuery } from './query' export type { LocationQuery, @@ -29,7 +31,7 @@ export { viewDepthKey, } from './injectionSymbols' -export { START_LOCATION_NORMALIZED as START_LOCATION } from './types' +export { START_LOCATION_NORMALIZED as START_LOCATION } from './location' export type { // route location _RouteLocationBase, @@ -52,7 +54,6 @@ export type { _RouteRecordBase, RouteRecordName, RouteRecordRaw, - RouteRecordRedirectOption, RouteRecordSingleView, RouteRecordSingleViewWithChildren, RouteRecordMultipleViews, diff --git a/packages/router/src/injectionSymbols.ts b/packages/router/src/injectionSymbols.ts index ad391624f..49ec55e92 100644 --- a/packages/router/src/injectionSymbols.ts +++ b/packages/router/src/injectionSymbols.ts @@ -1,5 +1,5 @@ import type { InjectionKey, ComputedRef, Ref } from 'vue' -import { RouteLocationNormalizedLoaded } from './types' +import type { RouteLocationNormalizedLoaded } from './typed-routes' import { RouteRecordNormalized } from './matcher/types' import type { Router } from './router' diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index 1f29cfb63..29fcb41ec 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -8,6 +8,7 @@ import { RouteRecord } from './matcher/types' import { warn } from './warning' import { isArray } from './utils' import { decode } from './encoding' +import { RouteLocationNormalizedLoaded } from './typed-routes' /** * Location object returned by {@link `parseURL`}. @@ -247,3 +248,32 @@ export function resolveRelativePath(to: string, from: string): string { toSegments.slice(toPosition).join('/') ) } + +/** + * Initial route location where the router is. Can be used in navigation guards + * to differentiate the initial navigation. + * + * @example + * ```js + * import { START_LOCATION } from 'vue-router' + * + * router.beforeEach((to, from) => { + * if (from === START_LOCATION) { + * // initial navigation + * } + * }) + * ``` + */ +export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = { + path: '/', + // @ts-expect-error: internal name for compatibility + name: undefined, + // TODO: could we use a symbol in the future? + params: {}, + query: {}, + hash: '', + fullPath: '/', + matched: [], + meta: {}, + redirectedFrom: undefined, +} diff --git a/packages/router/src/matcher/index.ts b/packages/router/src/matcher/index.ts index bdc292c86..0b8b5339c 100644 --- a/packages/router/src/matcher/index.ts +++ b/packages/router/src/matcher/index.ts @@ -3,8 +3,8 @@ import { MatcherLocationRaw, MatcherLocation, isRouteName, - RouteRecordName, _RouteRecordProps, + RouteRecordName, } from '../types' import { createRouterError, ErrorTypes, MatcherError } from '../errors' import { createRouteRecordMatcher, RouteRecordMatcher } from './pathMatcher' @@ -28,10 +28,10 @@ import { assign, noop } from '../utils' */ export interface RouterMatcher { addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void - removeRoute: { - (matcher: RouteRecordMatcher): void - (name: RouteRecordName): void - } + + removeRoute(matcher: RouteRecordMatcher): void + removeRoute(name: RouteRecordName): void + getRoutes: () => RouteRecordMatcher[] getRecordMatcher: (name: RouteRecordName) => RouteRecordMatcher | undefined diff --git a/packages/router/src/navigationGuards.ts b/packages/router/src/navigationGuards.ts index 27eac6cfc..4bfd504b2 100644 --- a/packages/router/src/navigationGuards.ts +++ b/packages/router/src/navigationGuards.ts @@ -1,15 +1,17 @@ import { NavigationGuard, - RouteLocationNormalized, NavigationGuardNext, - RouteLocationRaw, - RouteLocationNormalizedLoaded, NavigationGuardNextCallback, isRouteLocation, Lazy, RouteComponent, RawRouteComponent, } from './types' +import type { + RouteLocationRaw, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, +} from './typed-routes' import { createRouterError, diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 20ab4d1e2..c7b1467f0 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -1,20 +1,21 @@ import { - RouteLocationNormalized, RouteRecordRaw, - RouteLocationRaw, NavigationHookAfter, - START_LOCATION_NORMALIZED, Lazy, - RouteLocationNormalizedLoaded, - RouteLocation, - RouteRecordName, isRouteLocation, isRouteName, NavigationGuardWithThis, RouteLocationOptions, MatcherLocationRaw, - RouteParams, } from './types' +import type { + RouteLocation, + RouteLocationRaw, + RouteRecordName, + RouteParams, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, +} from './typed-routes' import { RouterHistory, HistoryState, NavigationType } from './history/common' import { ScrollPosition, @@ -49,6 +50,7 @@ import { stringifyURL, isSameRouteLocation, isSameRouteRecord, + START_LOCATION_NORMALIZED, } from './location' import { extractComponentsGuards, guardToPromiseFn } from './navigationGuards' import { warn } from './warning' @@ -60,6 +62,7 @@ import { routerViewLocationKey, } from './injectionSymbols' import { addDevtools } from './devtools' +import { _LiteralUnion } from './types/utils' /** * Internal type to define an ErrorHandler @@ -432,7 +435,7 @@ export function createRouter(options: RouterOptions): Router { } function resolve( - rawLocation: Readonly, + rawLocation: RouteLocationRaw, currentLocation?: RouteLocationNormalizedLoaded ): RouteLocation & { href: string } { // const objectLocation = routerLocationAsObject(rawLocation) @@ -466,7 +469,7 @@ export function createRouter(options: RouterOptions): Router { hash: decode(locationNormalized.hash), redirectedFrom: undefined, href, - }) + }) as any // FIXME: } if (__DEV__ && !isRouteLocation(rawLocation)) { @@ -474,7 +477,7 @@ export function createRouter(options: RouterOptions): Router { `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`, rawLocation ) - rawLocation = {} + return resolve({}) } let matcherLocation: MatcherLocationRaw @@ -564,7 +567,8 @@ export function createRouter(options: RouterOptions): Router { ? normalizeQuery(rawLocation.query) : ((rawLocation.query || {}) as LocationQuery), }, - matchedRoute, + // make it typed + matchedRoute as RouteLocation, { redirectedFrom: undefined, href, @@ -623,7 +627,7 @@ export function createRouter(options: RouterOptions): Router { if ( __DEV__ && - newTargetLocation.path == null && + (!('path' in newTargetLocation) || newTargetLocation.path == null) && !('name' in newTargetLocation) ) { warn( diff --git a/packages/router/src/typed-routes/index.ts b/packages/router/src/typed-routes/index.ts new file mode 100644 index 000000000..96fd6ac15 --- /dev/null +++ b/packages/router/src/typed-routes/index.ts @@ -0,0 +1,22 @@ +export type { + ParamValue, + ParamValueOneOrMore, + ParamValueZeroOrMore, + ParamValueZeroOrOne, +} from './params' + +export type { RouteRecordInfo } from './route-map' + +export type { + _RouteRecordName as RouteRecordName, + _RouteLocationRaw as RouteLocationRaw, + _RouteLocation as RouteLocation, + _RouteLocationNormalized as RouteLocationNormalized, + _RouteLocationNormalizedLoaded as RouteLocationNormalizedLoaded, + _RouteLocationResolved as RouteLocationResolved, + _RouteLocationAsRelativePath as RouteLocationAsRelativePath, + _RouteParams as RouteParams, + _RouteParamsRaw as RouteParamsRaw, +} from './route-location' + +export type { RouteRecordRedirectOption } from './route-records' diff --git a/packages/router/src/typed-routes/params.ts b/packages/router/src/typed-routes/params.ts new file mode 100644 index 000000000..15b043885 --- /dev/null +++ b/packages/router/src/typed-routes/params.ts @@ -0,0 +1,34 @@ +// TODO: refactor to ParamValueRaw and ParamValue ? + +/** + * Utility type for raw and non raw params like :id+ + * + */ +export type ParamValueOneOrMore = [ + ParamValue, + ...ParamValue[] +] + +/** + * Utility type for raw and non raw params like :id* + * + */ +export type ParamValueZeroOrMore = true extends isRaw + ? ParamValue[] | undefined | null + : ParamValue[] | undefined + +/** + * Utility type for raw and non raw params like :id? + * + */ +export type ParamValueZeroOrOne = true extends isRaw + ? string | number | null | undefined + : string + +/** + * Utility type for raw and non raw params like :id + * + */ +export type ParamValue = true extends isRaw + ? string | number + : string diff --git a/packages/router/src/typed-routes/route-location.ts b/packages/router/src/typed-routes/route-location.ts new file mode 100644 index 000000000..2221db5de --- /dev/null +++ b/packages/router/src/typed-routes/route-location.ts @@ -0,0 +1,223 @@ +import type { + RouteLocation, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, + RouteLocationOptions, + RouteQueryAndHash, + RouteRecordName, + RouteLocationRaw, +} from '../types' +import type { _LiteralUnion } from '../types/utils' +// inlining the type as it avoids code splitting issues +import type { RouteMap, _RouteMapGeneric } from './route-map' +import type { Router } from '../router' + +/** + * Type safe version if it exists of the routes' names. + */ +export type _RouteRecordName = keyof RouteMap + +/** + * Type safe version of the {@link RouteLocation} type. + * @internal + */ +export interface RouteLocationTyped< + RouteMap extends _RouteMapGeneric, + Name extends keyof RouteMap +> extends RouteLocation { + name: Extract + params: RouteMap[Name]['params'] +} + +/** + * Type safe version of the {@link RouteLocation} type as a Record with all the routes. + * @internal + */ +export type RouteLocationTypedList< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = { [N in keyof RouteMap]: RouteLocationTyped } + +/** + * Helper to generate a type safe version of the `RouteLocationNormalized` type. + * @internal + */ +export interface RouteLocationNormalizedTyped< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric, + Name extends keyof RouteMap = keyof RouteMap +> extends RouteLocationNormalized { + name: Extract + // we don't override path because it could contain params and in practice it's just not useful + params: RouteMap[Name]['params'] +} + +/** + * Helper to generate a type safe version of the `RouteLocationNormalizedLoaded` type. + * @internal + */ +export type RouteLocationNormalizedTypedList< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = { [N in keyof RouteMap]: RouteLocationNormalizedTyped } + +/** + * Helper to generate a type safe version of the `RouteLocationNormalizedLoaded` type. + * @internal + */ +export interface RouteLocationNormalizedLoadedTyped< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric, + Name extends keyof RouteMap = keyof RouteMap +> extends RouteLocationNormalizedLoaded { + name: Extract + // we don't override path because it could contain params and in practice it's just not useful + params: RouteMap[Name]['params'] +} + +/** + * Helper to generate a type safe version of the {@link RouteLocationNormalizedLoaded } type. + * @internal + */ +export type RouteLocationNormalizedLoadedTypedList< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = { [N in keyof RouteMap]: RouteLocationNormalizedLoadedTyped } + +/** + * Type safe adaptation of {@link LocationAsRelativeRaw}. Used to generate the union of all possible location. + * @internal + */ +export interface RouteLocationAsRelativeTyped< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric, + Name extends keyof RouteMap = keyof RouteMap +> extends RouteQueryAndHash, + RouteLocationOptions { + name?: Name + params?: RouteMap[Name]['paramsRaw'] + + // A relative path shouldn't have a path. This is easier to check with TS + path?: undefined +} + +/** + * Type safe adaptation of {@link LocationAsRelativeRaw}. Used to generate the union of all possible location. + * @internal + */ +export type RouteLocationAsRelativeTypedList< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = { [N in keyof RouteMap]: RouteLocationAsRelativeTyped } + +/** + * Type safe version to auto complete the path of a route. + * @internal + */ +export interface RouteLocationAsPathTyped< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric, + Name extends keyof RouteMap = keyof RouteMap +> extends RouteQueryAndHash, + RouteLocationOptions { + path: _LiteralUnion + + // // allows to check for .path and other properties that exist in different route location types + // [key: string]: unknown +} + +/** + * Type safe version to auto complete the path of a route. + * @internal + */ +export type RouteLocationAsPathTypedList< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = { [N in keyof RouteMap]: RouteLocationAsPathTyped } + +/** + * Same as {@link RouteLocationAsPathTyped} but as a string literal. + * @internal + */ +export type RouteLocationAsString< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = _LiteralUnion + +/** + * Type safe version of a resolved route location returned by `router.resolve()`. + * @see {@link RouteLocationTyped} + * @internal + */ +export interface RouteLocationResolvedTyped< + RouteMap extends _RouteMapGeneric, + Name extends keyof RouteMap +> extends RouteLocationTyped { + href: string +} + +/** + * Record of all the resolved routes. + * @see {@link RouteLocationResolvedTyped} + * @internal + */ +export type RouteLocationResolvedTypedList< + RouteMap extends _RouteMapGeneric = _RouteMapGeneric +> = { [N in keyof RouteMap]: RouteLocationResolvedTyped } + +/** + * Type safe versions of types that are exposed by vue-router + */ + +/** + * Type safe version of `RouteLocationNormalized`. Accepts the name of the route as a type parameter. + * @see {@link RouteLocationNormalized} + */ +export type _RouteLocationNormalized< + Name extends _RouteRecordName = _RouteRecordName +> = RouteLocationNormalizedTypedList[Name] + +/** + * Type safe version of `RouteLocationNormalizedLoaded`. Accepts the name of the route as a type parameter. + * @see {@link RouteLocationNormalizedLoaded} + */ +export type _RouteLocationNormalizedLoaded< + Name extends _RouteRecordName = _RouteRecordName +> = RouteLocationNormalizedLoadedTypedList[Name] + +/** + * Type safe version of `RouteLocationAsRelative`. Accepts the name of the route as a type parameter. + * @see {@link RouteLocationAsRelative} + */ +export type _RouteLocationAsRelativePath< + Name extends _RouteRecordName = _RouteRecordName +> = RouteLocationAsRelativeTypedList[Name] + +/** + * Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`). + * Allows passing the name of the route to be passed as a generic. + * @see {@link Router['resolve']} + */ +export type _RouteLocationResolved< + Name extends keyof RouteMap = keyof RouteMap +> = RouteLocationResolvedTypedList[Name] + +/** + * Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic. + * @see {@link RouteLocation} + */ +export type _RouteLocation = + RouteLocationTypedList[Name] + +/** + * Type safe version of {@link `RouteLocationRaw`} . Allows passing the name of the route to be passed as a generic. + * @see {@link RouteLocationRaw} + */ +export type _RouteLocationRaw = + | RouteLocationAsString + | RouteLocationAsRelativeTypedList[Name] + | RouteLocationAsPathTypedList[Name] + +/** + * Generate a type safe params for a route location. Requires the name of the route to be passed as a generic. + * @see {@link RouteParams} + */ +export type _RouteParams = + RouteMap[Name]['params'] + +/** + * Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic. + * @see {@link RouteParamsRaw} + */ +export type _RouteParamsRaw = + RouteMap[Name]['paramsRaw'] diff --git a/packages/router/src/typed-routes/route-map.ts b/packages/router/src/typed-routes/route-map.ts new file mode 100644 index 000000000..7257c0f7c --- /dev/null +++ b/packages/router/src/typed-routes/route-map.ts @@ -0,0 +1,39 @@ +import type { TypesConfig } from '../config' +import type { RouteMeta, RouteParams, RouteParamsRaw } from '../types' +import type { RouteRecord } from '../matcher/types' + +/** + * Helper type to define a Typed `RouteRecord` + * @see {@link RouteRecord} + */ +export interface RouteRecordInfo< + Name extends string | symbol = string, + Path extends string = string, + // TODO: could probably be inferred from the Params + ParamsRaw extends RouteParamsRaw = RouteParamsRaw, + Params extends RouteParams = RouteParams, + Meta extends RouteMeta = RouteMeta +> { + name: Name + path: Path + paramsRaw: ParamsRaw + params: Params + // TODO: implement meta with a defineRoute macro + meta: Meta +} + +/** + * Convenience type to get the typed RouteMap or a generic one if not provided. + */ +export type RouteMap = TypesConfig extends Record< + 'RouteNamedMap', + infer RouteNamedMap +> + ? RouteNamedMap + : _RouteMapGeneric + +/** + * Generic version of the RouteMap. + * @internal + */ +export type _RouteMapGeneric = Record diff --git a/packages/router/src/typed-routes/route-records.ts b/packages/router/src/typed-routes/route-records.ts new file mode 100644 index 000000000..1395f274d --- /dev/null +++ b/packages/router/src/typed-routes/route-records.ts @@ -0,0 +1,8 @@ +import { _RouteLocation, _RouteLocationRaw } from './route-location' + +/** + * @internal + */ +export type RouteRecordRedirectOption = + | _RouteLocationRaw + | ((to: _RouteLocation) => _RouteLocationRaw) diff --git a/packages/router/src/types/index.ts b/packages/router/src/types/index.ts index 9c2396b21..8b1d2be68 100644 --- a/packages/router/src/types/index.ts +++ b/packages/router/src/types/index.ts @@ -4,6 +4,7 @@ import { Ref, ComponentPublicInstance, Component, DefineComponent } from 'vue' import { RouteRecord, RouteRecordNormalized } from '../matcher/types' import { HistoryState } from '../history/common' import { NavigationFailure } from '../errors' +import { RouteRecordRedirectOption } from '../typed-routes' export type Lazy = () => Promise export type Override = Pick> & U @@ -306,13 +307,6 @@ export interface _RouteRecordBase extends PathParserOptions { */ export interface RouteMeta extends Record {} -/** - * @internal - */ -export type RouteRecordRedirectOption = - | RouteLocationRaw - | ((to: RouteLocation) => RouteLocationRaw) - /** * Route Record defining one single component with the `component` option. */ @@ -407,33 +401,6 @@ export type RouteRecordRaw = | RouteRecordMultipleViewsWithChildren | RouteRecordRedirect -/** - * Initial route location where the router is. Can be used in navigation guards - * to differentiate the initial navigation. - * - * @example - * ```js - * import { START_LOCATION } from 'vue-router' - * - * router.beforeEach((to, from) => { - * if (from === START_LOCATION) { - * // initial navigation - * } - * }) - * ``` - */ -export const START_LOCATION_NORMALIZED: RouteLocationNormalizedLoaded = { - path: '/', - name: undefined, - params: {}, - query: {}, - hash: '', - fullPath: '/', - matched: [], - meta: {}, - redirectedFrom: undefined, -} - // make matched non-enumerable for easy printing // NOTE: commented for tests at RouterView.spec // Object.defineProperty(START_LOCATION_NORMALIZED, 'matched', { diff --git a/packages/router/src/types/typeGuards.ts b/packages/router/src/types/typeGuards.ts index 782ef08c0..7d1f06baa 100644 --- a/packages/router/src/types/typeGuards.ts +++ b/packages/router/src/types/typeGuards.ts @@ -1,9 +1,9 @@ -import { RouteLocationRaw, RouteRecordName } from './index' +import { RouteLocationRaw, RouteRecordName } from '../typed-routes' export function isRouteLocation(route: any): route is RouteLocationRaw { return typeof route === 'string' || (route && typeof route === 'object') } -export function isRouteName(name: any): name is RouteRecordName { +export function isRouteName(name: any): name is string | symbol { return typeof name === 'string' || typeof name === 'symbol' } diff --git a/packages/router/src/types/utils.ts b/packages/router/src/types/utils.ts index b5874a260..e7d163184 100644 --- a/packages/router/src/types/utils.ts +++ b/packages/router/src/types/utils.ts @@ -1,10 +1,17 @@ /** + * Creates a union type that still allows autocompletion for strings. * @internal */ export type _LiteralUnion = | LiteralType | (BaseType & Record) +/** + * Maybe a promise maybe not + * @internal + */ +export type _Awaitable = T | PromiseLike + /** * @internal */ diff --git a/packages/router/src/useApi.ts b/packages/router/src/useApi.ts index 9bf578539..853100b60 100644 --- a/packages/router/src/useApi.ts +++ b/packages/router/src/useApi.ts @@ -1,7 +1,8 @@ import { inject } from 'vue' import { routerKey, routeLocationKey } from './injectionSymbols' import { Router } from './router' -import { RouteLocationNormalizedLoaded } from './types' +import { RouteMap } from './typed-routes/route-map' +import { RouteLocationNormalized } from './typed-routes' /** * Returns the router instance. Equivalent to using `$router` inside @@ -15,6 +16,8 @@ export function useRouter(): Router { * Returns the current route location. Equivalent to using `$route` inside * templates. */ -export function useRoute(): RouteLocationNormalizedLoaded { +export function useRoute( + _name?: Name +): RouteLocationNormalized { return inject(routeLocationKey)! }