From: Eduardo San Martin Morote Date: Tue, 7 Jun 2022 14:01:16 +0000 (+0200) Subject: refactor(types): better type inference for named routes X-Git-Tag: v4.1.0~87 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ba50812a3e795ced797e8ec725afa7641ca6efeb;p=thirdparty%2Fvuejs%2Frouter.git refactor(types): better type inference for named routes --- diff --git a/src/router.ts b/src/router.ts index abd238b7..b29514c2 100644 --- a/src/router.ts +++ b/src/router.ts @@ -70,7 +70,7 @@ import { routerViewLocationKey, } from './injectionSymbols' import { addDevtools } from './devtools' -import { RouteNamedMap, RouteNamedMapGeneric } from './types/named' +import { RouteNamedMap } from './types/named' /** * Internal type to define an ErrorHandler @@ -255,13 +255,11 @@ export interface Router { * * @param to - Route location to navigate to */ - push< - RouteMap extends RouteNamedMapGeneric = RouteNamedMap, - Name extends keyof RouteMap = keyof RouteNamedMap - >( - to: RouteNamedMapGeneric extends RouteMap - ? RouteLocationRaw - : RouteLocationNamedRaw | RouteLocationPathRaw | string + push( + to: + | RouteLocationNamedRaw> + | string + | RouteLocationPathRaw ): Promise /** @@ -270,13 +268,11 @@ export interface Router { * * @param to - Route location to navigate to */ - replace< - RouteMap extends RouteNamedMapGeneric = RouteNamedMap, - Name extends keyof RouteMap = keyof RouteNamedMap - >( - to: RouteNamedMapGeneric extends RouteMap - ? RouteLocationRaw - : RouteLocationNamedRaw | RouteLocationPathRaw | string + replace( + to: + | RouteLocationNamedRaw> + | string + | RouteLocationPathRaw ): Promise /** diff --git a/src/types/index.ts b/src/types/index.ts index a74c2f0c..ddfd0dc1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,7 +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 { RouteNamedMapGeneric } from './named' +import { RouteNamedInfo, RouteNamedMapGeneric } from './named' export type Lazy = () => Promise export type Override = Pick> & U @@ -69,32 +69,24 @@ export interface LocationAsName { * @internal */ export interface LocationAsRelativeRaw< - RouteMap extends RouteNamedMapGeneric = RouteNamedMapGeneric, - Name extends keyof RouteMap = keyof RouteMap + Name extends RouteRecordName = RouteRecordName, + Info extends RouteNamedInfo = RouteNamedInfo > { - name?: RouteNamedMapGeneric extends RouteMap ? RouteRecordName : Name - params?: RouteNamedMapGeneric extends RouteMap - ? RouteParamsRaw - : RouteMap[Name]['paramsRaw'] + name?: Name + params?: Info extends RouteNamedInfo + ? ParamsRaw + : RouteParamsRaw } -// this one didn't work 🤔 -// export type LocationAsRelativeRaw< -// RouteMap extends RouteNamedMapGeneric = RouteNamedMapGeneric -// > = { -// [N in keyof RouteMap]: { -// name?: N -// params?: RouteMap[N]['paramsRaw'] -// } -// }[keyof RouteMap] - +/** + * @internal + */ export interface LocationAsRelative< - RouteMap extends RouteNamedMapGeneric = RouteNamedMapGeneric, - Name extends keyof RouteMap = keyof RouteMap + Info extends RouteNamedInfo = RouteNamedInfo > { - params?: RouteNamedMapGeneric extends RouteMap - ? RouteParams - : RouteMap[Name]['params'] + params?: Info extends RouteNamedInfo + ? Params + : RouteParams } export interface RouteLocationOptions { @@ -128,11 +120,15 @@ export type RouteLocationRaw = * @internal */ export type RouteLocationNamedRaw< - RouteMap extends RouteNamedMapGeneric = RouteNamedMapGeneric, - Name extends keyof RouteMap = keyof RouteMap -> = RouteQueryAndHash & - LocationAsRelativeRaw & - RouteLocationOptions + RouteMap extends RouteNamedMapGeneric = RouteNamedMapGeneric +> = RouteNamedMapGeneric extends RouteMap + ? // allows assigning a RouteLocationRaw to RouteLocationNamedRaw + RouteQueryAndHash & LocationAsRelativeRaw & RouteLocationOptions + : { + [K in Extract]: RouteQueryAndHash & + LocationAsRelativeRaw & + RouteLocationOptions + }[Extract] export type RouteLocationPathRaw = | RouteQueryAndHash & LocationAsPath & RouteLocationOptions diff --git a/src/types/named.ts b/src/types/named.ts index 4740dc3a..74b78634 100644 --- a/src/types/named.ts +++ b/src/types/named.ts @@ -1,4 +1,9 @@ -import type { RouteParams, RouteParamsRaw, RouteRecordRaw } from '.' +import type { + RouteParams, + RouteParamsRaw, + RouteRecordRaw, + RouteRecordName, +} from '.' import type { _JoinPath, ParamsFromPath, ParamsRawFromPath } from './paths' export type RouteNamedMap< @@ -6,32 +11,30 @@ export type RouteNamedMap< Prefix extends string = '' > = Routes extends readonly [infer R, ...infer Rest] ? Rest extends Readonly - ? (R extends { - name?: infer Name - path: infer Path - children?: infer Children - } - ? Path extends string - ? (Name extends string | symbol - ? { - [N in Name]: { - // name: N - params: ParamsFromPath<_JoinPath> - // TODO: ParamsRawFromPath - paramsRaw: ParamsRawFromPath<_JoinPath> - path: _JoinPath - } + ? (R extends _RouteNamedRecordBaseInfo< + infer Name, + infer Path, + infer Children + > + ? (Name extends RouteRecordName + ? { + [N in Name]: { + // name: N + params: ParamsFromPath<_JoinPath> + // TODO: ParamsRawFromPath + paramsRaw: ParamsRawFromPath<_JoinPath> + path: _JoinPath } + } + : { + // NO_NAME: 1 + }) & + // Recurse children + (Children extends Readonly + ? RouteNamedMap> : { - // NO_NAME: 1 - }) & - // Recurse children - (Children extends Readonly - ? RouteNamedMap> - : { - // NO_CHILDREN: 1 - }) - : never // Path must be a string + // NO_CHILDREN: 1 + }) : { // EMPTY: 1 }) & @@ -41,11 +44,30 @@ export type RouteNamedMap< // END: 1 } -export type RouteNamedMapGeneric = Record< - string | symbol | number, - { - params: RouteParams - paramsRaw: RouteParamsRaw - path: string - } -> +export interface _RouteNamedRecordBaseInfo< + Name extends RouteRecordName = RouteRecordName, // we don't care about symbols + Path extends string = string, + Children extends Readonly = Readonly +> { + name?: Name + path: Path + children?: Children +} + +/** + * Generic map of named routes from a list of route records. + */ +export type RouteNamedMapGeneric = Record + +/** + * Relevant information about a named route record to deduce its params. + */ +export interface RouteNamedInfo< + Path extends string = string, + Params extends RouteParams = RouteParams, + ParamsRaw extends RouteParamsRaw = RouteParamsRaw +> { + params: Params + paramsRaw: ParamsRaw + path: Path +} diff --git a/src/types/paths.ts b/src/types/paths.ts index 8ff156f6..024b827e 100644 --- a/src/types/paths.ts +++ b/src/types/paths.ts @@ -10,10 +10,20 @@ import { RouteParams, RouteParamsRaw, RouteParamValueRaw } from '.' */ export type ParamsFromPath

= string extends P ? RouteParams // Generic version + : _ExtractParamsPath<_RemoveRegexpFromParam

, false> extends Record< + any, + never + > + ? Record : _ExtractParamsPath<_RemoveRegexpFromParam

, false> export type ParamsRawFromPath

= string extends P ? RouteParamsRaw // Generic version + : _ExtractParamsPath<_RemoveRegexpFromParam

, true> extends Record< + any, + never + > + ? Record : _ExtractParamsPath<_RemoveRegexpFromParam

, true> /** diff --git a/test-dts/namedRoutes.test-d.ts b/test-dts/namedRoutes.test-d.ts index ad194a72..3fbb347d 100644 --- a/test-dts/namedRoutes.test-d.ts +++ b/test-dts/namedRoutes.test-d.ts @@ -82,17 +82,21 @@ for (const method of methods) { r2[method]({ params: { a: 2 } }) r2[method]({ params: {} }) r2[method]({ params: { opt: 'hey' } }) + // @ts-expect-error: but not non existent + r2[method]({ params: { fake_param: 'hey' } }) // routes with no params r2[method]({ name: 'nested' }) r2[method]({ name: 'nested', params: {} }) // FIXME: is it possible to support this version - // // @ts-expect-error: does not accept any params - // r2[method]({ name: 'nested', params: { eo: 'true' } }) + // @ts-expect-error: does not accept any params + r2[method]({ name: 'nested', params: { id: 2 } }) } -// still allow generics to be passed for convenience +// NOTE: not possible if we use the named routes as the point is to provide valid routes only +// @ts-expect-error r2.push({} as unknown as RouteLocationRaw) +// @ts-expect-error r2.replace({} as unknown as RouteLocationRaw) // createMap(r2.options.routes, true). diff --git a/test-dts/paths.test-d.ts b/test-dts/paths.test-d.ts index 0342cec2..0ae4918b 100644 --- a/test-dts/paths.test-d.ts +++ b/test-dts/paths.test-d.ts @@ -19,6 +19,10 @@ expectType<{ id: readonly [string, ...string[]] }>(params('/users/:id+')) expectType<{ id?: string | null | undefined }>(params('/users/:id?')) expectType<{ id?: readonly string[] | null | undefined }>(params('/users/:id*')) +expectType>(params('/hello/other/thing')) +// @ts-expect-error +expectType<{ thing: 'e' }>(params('/hello/other/thing')) + // @ts-expect-error expectType<{ other: string }>(params('/users/:id')) // @ts-expect-error