]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(types): typed string routes
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 10 Jun 2022 14:10:15 +0000 (16:10 +0200)
committerEduardo San Martin Morote <posva@users.noreply.github.com>
Thu, 30 Jun 2022 07:59:00 +0000 (09:59 +0200)
packages/router/src/router.ts
packages/router/src/types/index.ts
packages/router/src/types/named.ts
packages/router/src/types/paths.ts
packages/router/src/types/utils.ts [new file with mode: 0644]
packages/router/test-dts/namedRoutes.test-d.ts

index b29514c26f95f46de9f788d8d97797f1f526fe48..72d4534f62cdf79947c7b431ce61037a2671a922 100644 (file)
@@ -15,6 +15,7 @@ import {
   RouteParams,
   RouteLocationNamedRaw,
   RouteLocationPathRaw,
+  RouteLocationString,
 } from './types'
 import { RouterHistory, HistoryState, NavigationType } from './history/common'
 import {
@@ -70,7 +71,7 @@ import {
   routerViewLocationKey,
 } from './injectionSymbols'
 import { addDevtools } from './devtools'
-import { RouteNamedMap } from './types/named'
+import { RouteNamedMap, RouteStaticPathMap } from './types/named'
 
 /**
  * Internal type to define an ErrorHandler
@@ -258,8 +259,8 @@ export interface Router<Options extends RouterOptions = RouterOptions> {
   push(
     to:
       | RouteLocationNamedRaw<RouteNamedMap<Options['routes']>>
-      | string
-      | RouteLocationPathRaw
+      | RouteLocationString<RouteStaticPathMap<Options['routes']>>
+      | RouteLocationPathRaw<RouteStaticPathMap<Options['routes']>>
   ): Promise<NavigationFailure | void | undefined>
 
   /**
@@ -271,8 +272,8 @@ export interface Router<Options extends RouterOptions = RouterOptions> {
   replace(
     to:
       | RouteLocationNamedRaw<RouteNamedMap<Options['routes']>>
-      | string
-      | RouteLocationPathRaw
+      | RouteLocationString<RouteStaticPathMap<Options['routes']>>
+      | RouteLocationPathRaw<RouteStaticPathMap<Options['routes']>>
   ): Promise<NavigationFailure | void | undefined>
 
   /**
index 1d386270e6e24d16a9c6228bdfd7eeeb9023d66e..498fc399cf2c44ea6cc071a54886a72c169e483f 100644 (file)
@@ -4,7 +4,11 @@ import { Ref, ComponentPublicInstance, Component, DefineComponent } from 'vue'
 import { RouteRecord, RouteRecordNormalized } from '../matcher/types'
 import { HistoryState } from '../history/common'
 import { NavigationFailure } from '../errors'
-import { RouteNamedInfo, RouteNamedMapGeneric } from './named'
+import {
+  RouteNamedInfo,
+  RouteNamedMapGeneric,
+  RouteStaticPathMapGeneric,
+} from './named'
 
 export type Lazy<T> = () => Promise<T>
 export type Override<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U
@@ -53,8 +57,8 @@ export interface RouteQueryAndHash {
 /**
  * @internal
  */
-export interface LocationAsPath {
-  path: string
+export interface LocationAsPath<P extends string = string> {
+  path: P
 }
 
 /**
@@ -120,7 +124,20 @@ export type RouteLocationRaw =
   | RouteLocationNamedRaw
 
 /**
- * Route Location that can infer the necessary params based on the name
+ * Route location that can infer full path locations
+ *
+ * @internal
+ */
+export type RouteLocationString<
+  RouteMap extends RouteStaticPathMapGeneric = RouteStaticPathMapGeneric
+> = RouteStaticPathMapGeneric extends RouteMap
+  ? string
+  : {
+      [K in keyof RouteMap]: RouteMap[K]['fullPath']
+    }[keyof RouteMap]
+
+/**
+ * Route Location that can infer the necessary params based on the name.
  *
  * @internal
  */
@@ -135,8 +152,21 @@ export type RouteLocationNamedRaw<
         RouteLocationOptions
     }[Extract<keyof RouteMap, RouteRecordName>]
 
-export type RouteLocationPathRaw =
-  | RouteQueryAndHash & LocationAsPath & RouteLocationOptions
+/**
+ * Route Location that can infer the possible paths.
+ *
+ * @internal
+ */
+export type RouteLocationPathRaw<
+  RouteMap extends RouteStaticPathMapGeneric = RouteStaticPathMapGeneric
+> = RouteStaticPathMapGeneric extends RouteMap
+  ? // allows assigning a RouteLocationRaw to RouteLocationPat
+    RouteQueryAndHash & LocationAsPath & RouteLocationOptions
+  : {
+      [K in Extract<keyof RouteMap, string>]: RouteQueryAndHash &
+        LocationAsPath<RouteMap[K]['path']> &
+        RouteLocationOptions
+    }[Extract<keyof RouteMap, string>]
 
 export interface RouteLocationMatched extends RouteRecordNormalized {
   // components cannot be Lazy<RouteComponent>
index 74b786347f05effa71a6058cae72d31b305de37b..d343262f25098a9ddad08df4437f237f235fb5d0 100644 (file)
@@ -4,14 +4,25 @@ import type {
   RouteRecordRaw,
   RouteRecordName,
 } from '.'
-import type { _JoinPath, ParamsFromPath, ParamsRawFromPath } from './paths'
+import type {
+  _JoinPath,
+  ParamsFromPath,
+  ParamsRawFromPath,
+  PathFromParams,
+} from './paths'
+import { LiteralUnion } from './utils'
 
+/**
+ * Creates a map with each named route as a properties. Each property contains the type of the params in raw and
+ * normalized versions as well as the raw path.
+ * @internal
+ */
 export type RouteNamedMap<
   Routes extends Readonly<RouteRecordRaw[]>,
   Prefix extends string = ''
 > = Routes extends readonly [infer R, ...infer Rest]
   ? Rest extends Readonly<RouteRecordRaw[]>
-    ? (R extends _RouteNamedRecordBaseInfo<
+    ? (R extends _RouteRecordNamedBaseInfo<
         infer Name,
         infer Path,
         infer Children
@@ -44,7 +55,71 @@ export type RouteNamedMap<
       // END: 1
     }
 
-export interface _RouteNamedRecordBaseInfo<
+/**
+ * Type that adds valid semi literal paths to still enable autocomplete while allowing proper paths
+ */
+type _PathForAutocomplete<P extends string> = P extends `${string}:${string}`
+  ? LiteralUnion<P, PathFromParams<P>>
+  : P
+
+/**
+ * @internal
+ */
+export type _PathWithHash<P extends string> = `${P}#${string}`
+
+/**
+ * @internal
+ */
+export type _PathWithQuery<P extends string> = `${P}?${string}`
+
+/**
+ * @internal
+ */
+export type _FullPath<P extends string> = LiteralUnion<
+  P,
+  _PathWithHash<P> | _PathWithQuery<P>
+>
+
+/**
+ * @internal
+ */
+export type RouteStaticPathMap<
+  Routes extends Readonly<RouteRecordRaw[]>,
+  Prefix extends string = ''
+> = Routes extends readonly [infer R, ...infer Rest]
+  ? Rest extends Readonly<RouteRecordRaw[]>
+    ? (R extends _RouteRecordNamedBaseInfo<
+        infer _Name,
+        infer Path,
+        infer Children
+      >
+        ? {
+            // TODO: add | ${string} for params
+            // TODO: add extra type to append  ? and # variants
+            [P in Path as _JoinPath<Prefix, Path>]: {
+              path: _PathForAutocomplete<_JoinPath<Prefix, Path>>
+              fullPath: _FullPath<_PathForAutocomplete<_JoinPath<Prefix, Path>>>
+            }
+          } & (Children extends Readonly<RouteRecordRaw[]> // Recurse children
+            ? RouteStaticPathMap<Children, _JoinPath<Prefix, Path>>
+            : {
+                // NO_CHILDREN: 1
+              })
+        : never) & // R must be a valid route record
+        // recurse children
+        RouteStaticPathMap<Rest, Prefix>
+    : {
+        // EMPTY: 1
+      }
+  : {
+      // END: 1
+    }
+
+/**
+ * Important information in a Named Route Record
+ * @internal
+ */
+export interface _RouteRecordNamedBaseInfo<
   Name extends RouteRecordName = RouteRecordName, // we don't care about symbols
   Path extends string = string,
   Children extends Readonly<RouteRecordRaw[]> = Readonly<RouteRecordRaw[]>
@@ -56,11 +131,24 @@ export interface _RouteNamedRecordBaseInfo<
 
 /**
  * Generic map of named routes from a list of route records.
+ *
+ * @internal
  */
 export type RouteNamedMapGeneric = Record<RouteRecordName, RouteNamedInfo>
 
+/**
+ * Generic map of routes paths from a list of route records.
+ *
+ * @internal
+ */
+export type RouteStaticPathMapGeneric = Record<
+  string,
+  { path: string; fullPath: string }
+>
+
 /**
  * Relevant information about a named route record to deduce its params.
+ * @internal
  */
 export interface RouteNamedInfo<
   Path extends string = string,
index 024b827e0350be3cfd9418c968b3f4e654fd8702..e1888fa2d78e96b40dcc363cf9027aab2bb3e855 100644 (file)
@@ -288,7 +288,7 @@ export type _BuildPath<
  */
 export type PathFromParams<
   P extends string,
-  PO extends ParamsFromPath<P>
+  PO extends ParamsFromPath<P> = ParamsFromPath<P>
 > = string extends P ? string : _BuildPath<_RemoveRegexpFromParam<P>, PO>
 
 /**
diff --git a/packages/router/src/types/utils.ts b/packages/router/src/types/utils.ts
new file mode 100644 (file)
index 0000000..63f1ef2
--- /dev/null
@@ -0,0 +1,3 @@
+export type LiteralUnion<LiteralType, BaseType extends string = string> =
+  | LiteralType
+  | (BaseType & Record<never, never>)
index 3fbb347d1f41e3a28c4fa77adc5c78fbed1b6c57..76b22a36c3d2e08ea57282546794c4d3b67acdd2 100644 (file)
@@ -18,6 +18,8 @@ const routeName = Symbol()
 const r2 = createRouter({
   history: createWebHistory(),
   routes: [
+    { path: '/', component },
+    { path: '/foo', component },
     { path: '/users/:id', name: 'UserDetails', component },
     { path: '/no-name', /* no name */ components },
     {
@@ -74,10 +76,6 @@ for (const method of methods) {
   r2[method]({ name: routeName })
   // @ts-expect-error: but not other symbols
   r2[method]({ name: Symbol() })
-  // any path is still valid
-  r2[method]('/path')
-  r2.push('/path')
-  r2.replace('/path')
   // relative push can have any of the params
   r2[method]({ params: { a: 2 } })
   r2[method]({ params: {} })
@@ -91,6 +89,27 @@ for (const method of methods) {
   // FIXME: is it possible to support this version
   // @ts-expect-error: does not accept any params
   r2[method]({ name: 'nested', params: { id: 2 } })
+
+  // paths
+  r2[method]({ path: '/nested' })
+  r2[method]({ path: '/nested/a/b' })
+  // @ts-expect-error
+  r2[method]({ path: '' })
+  // @ts-expect-error
+  r2[method]({ path: '/nope' })
+  // @ts-expect-error
+  r2[method]({ path: '/no-name?query' })
+  // @ts-expect-error
+  r2[method]({ path: '/no-name#hash' })
+
+  r2[method]('/nested')
+  r2[method]('/nested/a/b')
+  // @ts-expect-error
+  r2[method]('')
+  // @ts-expect-error
+  r2[method]('/nope')
+  r2[method]('/no-name?query')
+  r2[method]('/no-name#hash')
 }
 
 // NOTE: not possible if we use the named routes as the point is to provide valid routes only