From: Eduardo San Martin Morote Date: Fri, 10 Jun 2022 14:10:15 +0000 (+0200) Subject: feat(types): typed string routes X-Git-Tag: v4.1.0~36 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fdcb94651cb44d4149782403978b37dc4b3312e7;p=thirdparty%2Fvuejs%2Frouter.git feat(types): typed string routes --- diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index b29514c2..72d4534f 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -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 { push( to: | RouteLocationNamedRaw> - | string - | RouteLocationPathRaw + | RouteLocationString> + | RouteLocationPathRaw> ): Promise /** @@ -271,8 +272,8 @@ export interface Router { replace( to: | RouteLocationNamedRaw> - | string - | RouteLocationPathRaw + | RouteLocationString> + | RouteLocationPathRaw> ): Promise /** diff --git a/packages/router/src/types/index.ts b/packages/router/src/types/index.ts index 1d386270..498fc399 100644 --- a/packages/router/src/types/index.ts +++ b/packages/router/src/types/index.ts @@ -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 = () => Promise export type Override = Pick> & U @@ -53,8 +57,8 @@ export interface RouteQueryAndHash { /** * @internal */ -export interface LocationAsPath { - path: string +export interface LocationAsPath

{ + 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] -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]: RouteQueryAndHash & + LocationAsPath & + RouteLocationOptions + }[Extract] export interface RouteLocationMatched extends RouteRecordNormalized { // components cannot be Lazy diff --git a/packages/router/src/types/named.ts b/packages/router/src/types/named.ts index 74b78634..d343262f 100644 --- a/packages/router/src/types/named.ts +++ b/packages/router/src/types/named.ts @@ -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, Prefix extends string = '' > = Routes extends readonly [infer R, ...infer Rest] ? Rest extends Readonly - ? (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}:${string}` + ? LiteralUnion> + : P + +/** + * @internal + */ +export type _PathWithHash

= `${P}#${string}` + +/** + * @internal + */ +export type _PathWithQuery

= `${P}?${string}` + +/** + * @internal + */ +export type _FullPath

= LiteralUnion< + P, + _PathWithHash

| _PathWithQuery

+> + +/** + * @internal + */ +export type RouteStaticPathMap< + Routes extends Readonly, + Prefix extends string = '' +> = Routes extends readonly [infer R, ...infer Rest] + ? Rest extends Readonly + ? (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]: { + path: _PathForAutocomplete<_JoinPath> + fullPath: _FullPath<_PathForAutocomplete<_JoinPath>> + } + } & (Children extends Readonly // Recurse children + ? RouteStaticPathMap> + : { + // NO_CHILDREN: 1 + }) + : never) & // R must be a valid route record + // recurse children + RouteStaticPathMap + : { + // 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 = Readonly @@ -56,11 +131,24 @@ export interface _RouteNamedRecordBaseInfo< /** * Generic map of named routes from a list of route records. + * + * @internal */ export type RouteNamedMapGeneric = Record +/** + * 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, diff --git a/packages/router/src/types/paths.ts b/packages/router/src/types/paths.ts index 024b827e..e1888fa2 100644 --- a/packages/router/src/types/paths.ts +++ b/packages/router/src/types/paths.ts @@ -288,7 +288,7 @@ export type _BuildPath< */ export type PathFromParams< P extends string, - PO extends ParamsFromPath

+ PO extends ParamsFromPath

= ParamsFromPath

> = string extends P ? string : _BuildPath<_RemoveRegexpFromParam

, PO> /** diff --git a/packages/router/src/types/utils.ts b/packages/router/src/types/utils.ts new file mode 100644 index 00000000..63f1ef2c --- /dev/null +++ b/packages/router/src/types/utils.ts @@ -0,0 +1,3 @@ +export type LiteralUnion = + | LiteralType + | (BaseType & Record) diff --git a/packages/router/test-dts/namedRoutes.test-d.ts b/packages/router/test-dts/namedRoutes.test-d.ts index 3fbb347d..76b22a36 100644 --- a/packages/router/test-dts/namedRoutes.test-d.ts +++ b/packages/router/test-dts/namedRoutes.test-d.ts @@ -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