]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: wip typed routes
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 3 Jun 2024 15:04:55 +0000 (17:04 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 10 Jun 2024 13:35:26 +0000 (15:35 +0200)
25 files changed:
packages/playground/src/main.ts
packages/router/__tests__/RouterLink.spec.ts
packages/router/__tests__/RouterView.spec.ts
packages/router/__tests__/errors.spec.ts
packages/router/__tests__/guards/extractComponentsGuards.spec.ts
packages/router/__tests__/guards/guardToPromiseFn.spec.ts
packages/router/__tests__/matcher/resolve.spec.ts
packages/router/__tests__/router.spec.ts
packages/router/src/config.ts
packages/router/src/errors.ts
packages/router/src/index.ts
packages/router/src/injectionSymbols.ts
packages/router/src/location.ts
packages/router/src/matcher/index.ts
packages/router/src/navigationGuards.ts
packages/router/src/router.ts
packages/router/src/typed-routes/index.ts [new file with mode: 0644]
packages/router/src/typed-routes/params.ts [new file with mode: 0644]
packages/router/src/typed-routes/route-location.ts [new file with mode: 0644]
packages/router/src/typed-routes/route-map.ts [new file with mode: 0644]
packages/router/src/typed-routes/route-records.ts [new file with mode: 0644]
packages/router/src/types/index.ts
packages/router/src/types/typeGuards.ts
packages/router/src/types/utils.ts
packages/router/src/useApi.ts

index ce131cb69ac4fb4a75484583d3d1859dd28139e0..103bb410dd8c862ee514314760dcc76ebc64f5b1 100644 (file)
@@ -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<never, never>, Record<never, never>>
+  '/[name]': RouteRecordInfo<
+    '/[name]',
+    '/:name',
+    { name: ParamValue<true> },
+    { name: ParamValue<false> }
+  >
+  '/[...path]': RouteRecordInfo<
+    '/[...path]',
+    '/:path(.*)',
+    { path: ParamValue<true> },
+    { path: ParamValue<false> }
+  >
+}
+
+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')
+}
index 1b9b8c0f64af3b519fe686e54d9434fa87e68ce4..76b335b1d023354c84190c831776d6f2963327fe 100644 (file)
@@ -3,11 +3,11 @@
  */
 import { RouterLink } 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'
index 54fea992cb2d6a5c1b8bbbf3ae0bcf5c0eebb440..8f1a4d1ba097b1ac32f4b1d052063ae5c5c64d18 100644 (file)
@@ -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'
index c7219dc4442f7f2c32f4494418cfcff2d2516037..c176832eb1077cec2e1968a84fe3d20aec69a46e 100644 (file)
@@ -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<RouteRecordRaw>[] = [
   { path: '/', component: components.Home },
index 560d9cc1e2a0e6f5ce98d19be097fe1320d20d5f..a8bfb4d870213916ecf29c3f84a93ed439d190e1 100644 (file)
@@ -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'
index d2b596f98c5ad20312161a7ee306186c29867309..b95d47c71692c0808948836f3feb038d8c276524 100644 (file)
@@ -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'
 
index de00849e13c001f800b480bed6801e062d1530e2..517c679bb7d50da05c5697e35db9e5264cf957d0 100644 (file)
@@ -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
    */
index bd6b186e06ec100b15f8beabd7ef1c24503eeee8..9f61c7aa7031bf3f5de184253e4da34c31168eb4 100644 (file)
@@ -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
 
index 1da3f7f1568d247fa9150ed2a6290ec11395b12f..02204e9ba8318af08acfa540a1cdc7381b9078a1 100644 (file)
@@ -1,5 +1,13 @@
 /**
- * Allows customizing existing types of the router that are used globally like `$router`, `<RouterLink>`, and `beforeRouteLeave()`. **ONLY FOR INTERNAL USAGE**.
+ * Allows customizing existing types of the router that are used globally like `$router`, `<RouterLink>`, 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
  */
index 021e7a8bb562c60e448ffcaeedbd79018d7c8eef..877a0de2169a9723a9b912d35be2d31387026a27 100644 (file)
@@ -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'
 
 /**
index 816dd2c516c1c12a5e73ec775072c9e08c26d10b..140a7318351d416d4a2c19b75d3b0de5732d25be 100644 (file)
@@ -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,
index ad391624f077c2570ac43f35e685b16009d55798..49ec55e92ddfb0a7006dce6dbc62ed0c61d8a29d 100644 (file)
@@ -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'
 
index 1f29cfb634251ea7bbd8f5df529d765119a6a445..29fcb41ecd55f8be5c2344e17a23ef9df6ad7802 100644 (file)
@@ -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,
+}
index 7040fa8104e09c4877d9fca58ed7cea1ff297294..44f8a50e23fd6807bd49489950e62deb4ccf4992 100644 (file)
@@ -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
 
index 27eac6cfc938bfbd05aafe20c2f9097869143ed9..4bfd504b2834a3d8ae7d299085715d8e4564b795 100644 (file)
@@ -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,
index 20ab4d1e2ec4cfcc17cd83bb7942f559737d75fd..c7b1467f00041a35c8f55bfbc2f8f64c20169097 100644 (file)
@@ -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<RouteLocationRaw>,
+    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 (file)
index 0000000..96fd6ac
--- /dev/null
@@ -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 (file)
index 0000000..15b0438
--- /dev/null
@@ -0,0 +1,34 @@
+// TODO: refactor to ParamValueRaw and ParamValue ?
+
+/**
+ * Utility type for raw and non raw params like :id+
+ *
+ */
+export type ParamValueOneOrMore<isRaw extends boolean> = [
+  ParamValue<isRaw>,
+  ...ParamValue<isRaw>[]
+]
+
+/**
+ * Utility type for raw and non raw params like :id*
+ *
+ */
+export type ParamValueZeroOrMore<isRaw extends boolean> = true extends isRaw
+  ? ParamValue<isRaw>[] | undefined | null
+  : ParamValue<isRaw>[] | undefined
+
+/**
+ * Utility type for raw and non raw params like :id?
+ *
+ */
+export type ParamValueZeroOrOne<isRaw extends boolean> = true extends isRaw
+  ? string | number | null | undefined
+  : string
+
+/**
+ * Utility type for raw and non raw params like :id
+ *
+ */
+export type ParamValue<isRaw extends boolean> = 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 (file)
index 0000000..2221db5
--- /dev/null
@@ -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<Name, RouteRecordName>
+  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<RouteMap, N> }
+
+/**
+ * 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<Name, RouteRecordName>
+  // 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<RouteMap, N> }
+
+/**
+ * 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<Name, RouteRecordName>
+  // 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<RouteMap, N> }
+
+/**
+ * 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<RouteMap, N> }
+
+/**
+ * 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<RouteMap[Name]['path']>
+
+  // // 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<RouteMap, N> }
+
+/**
+ * Same as {@link RouteLocationAsPathTyped} but as a string literal.
+ * @internal
+ */
+export type RouteLocationAsString<
+  RouteMap extends _RouteMapGeneric = _RouteMapGeneric
+> = _LiteralUnion<RouteMap[keyof RouteMap]['path'], string>
+
+/**
+ * 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<RouteMap, Name> {
+  href: string
+}
+
+/**
+ * Record of all the resolved routes.
+ * @see {@link RouteLocationResolvedTyped}
+ * @internal
+ */
+export type RouteLocationResolvedTypedList<
+  RouteMap extends _RouteMapGeneric = _RouteMapGeneric
+> = { [N in keyof RouteMap]: RouteLocationResolvedTyped<RouteMap, N> }
+
+/**
+ * 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<RouteMap>[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<RouteMap>[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<RouteMap>[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<RouteMap>[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<Name extends keyof RouteMap = keyof RouteMap> =
+  RouteLocationTypedList<RouteMap>[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<Name extends keyof RouteMap = keyof RouteMap> =
+  | RouteLocationAsString<RouteMap>
+  | RouteLocationAsRelativeTypedList<RouteMap>[Name]
+  | RouteLocationAsPathTypedList<RouteMap>[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<Name extends keyof RouteMap = keyof RouteMap> =
+  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<Name extends keyof RouteMap = keyof RouteMap> =
+  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 (file)
index 0000000..7257c0f
--- /dev/null
@@ -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<string | symbol, RouteRecordInfo>
diff --git a/packages/router/src/typed-routes/route-records.ts b/packages/router/src/typed-routes/route-records.ts
new file mode 100644 (file)
index 0000000..1395f27
--- /dev/null
@@ -0,0 +1,8 @@
+import { _RouteLocation, _RouteLocationRaw } from './route-location'
+
+/**
+ * @internal
+ */
+export type RouteRecordRedirectOption =
+  | _RouteLocationRaw
+  | ((to: _RouteLocation) => _RouteLocationRaw)
index 9c2396b21ce92b92f35f9b8f16098aab3766370d..8b1d2be6861c6489c5d2a0bf002820b3ac15acd6 100644 (file)
@@ -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<T> = () => Promise<T>
 export type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
@@ -306,13 +307,6 @@ export interface _RouteRecordBase extends PathParserOptions {
  */
 export interface RouteMeta extends Record<string | number | symbol, unknown> {}
 
-/**
- * @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', {
index 782ef08c00ad4be298a572fc3753d346448d9be9..7d1f06baaca4110d18051b6f9f465550e83bacb0 100644 (file)
@@ -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'
 }
index b5874a260fff059ef41dce378790a7de461082da..e7d163184072c83afe23ab4b0868027833a38671 100644 (file)
@@ -1,10 +1,17 @@
 /**
+ * Creates a union type that still allows autocompletion for strings.
  * @internal
  */
 export type _LiteralUnion<LiteralType, BaseType extends string = string> =
   | LiteralType
   | (BaseType & Record<never, never>)
 
+/**
+ * Maybe a promise maybe not
+ * @internal
+ */
+export type _Awaitable<T> = T | PromiseLike<T>
+
 /**
  * @internal
  */
index 9bf578539353b0f461ee6916d9b700877044ab5a..853100b6004b6a8be61eb9fd381db2d44bf8b358 100644 (file)
@@ -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 extends keyof RouteMap = keyof RouteMap>(
+  _name?: Name
+): RouteLocationNormalized<Name> {
   return inject(routeLocationKey)!
 }