From: Eduardo San Martin Morote Date: Mon, 2 May 2022 16:45:58 +0000 (+0200) Subject: feat(types): infer params types from path X-Git-Tag: v4.1.0~126 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=bf08d5291f4b020400e1e42058929a3a9a0295aa;p=thirdparty%2Fvuejs%2Frouter.git feat(types): infer params types from path --- diff --git a/src/index.ts b/src/index.ts index d3d2851d..7c583a21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,6 +59,12 @@ export type { NavigationGuardWithThis, NavigationHookAfter, } from './types' +export type { + ParamsFromPath, + _ExtractFirstParamName, + _RemoveRegexpFromParam, + _RemoveUntilClosingPar, +} from './types/paths' export { createRouter } from './router' export type { Router, RouterOptions, RouterScrollBehavior } from './router' diff --git a/src/types/paths.ts b/src/types/paths.ts new file mode 100644 index 00000000..72d965fb --- /dev/null +++ b/src/types/paths.ts @@ -0,0 +1,277 @@ +/** + * Generic possible params from a path (after parsing). + */ +export type PathParams = Record< + string, + string | readonly string[] | undefined | null +> + +/** + * Possible param modifiers. + * + * @internal + */ +export type _ParamModifier = '+' | '?' | '*' + +/** + * Characters that mark the end of a param. In reality, there is a lot more than + * this as only alphanumerical + _ are accepted as params but that is impossible + * to achieve with TS and in practice, This set should cover them all. TODO: Add + * missing characters that do not need to be encoded. + * + * @internal + */ +type _ParamDelimiter = + | '-' + | '/' + | '%' + | ':' + | '(' + | '\\' + | ';' + | ',' + | '&' + | '!' + | "'" + | '=' + | '@' + | '[' + | ']' + | _ParamModifier + +/** + * Given a simple path, creates an object of the possible param values. + * + * @internal + */ +export type _ExtractParamsPath

= + P extends `${string}{${infer PP}}${infer Rest}` + ? (PP extends `${infer N}${_ParamModifier}` + ? PP extends `${N}${infer M}` + ? M extends _ParamModifier + ? _ParamToObject + : never + : never + : _ParamToObject) & + _ExtractParamsPath + : {} + +/** + * Extract an object of params given a path like `/users/:id`. + */ +export type ParamsFromPath

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

> + +/** + * Gets the possible type of a param based on its modifier M. + * + * @internal + */ +export type _ModifierParamValue< + M extends _ParamModifier | '' = _ParamModifier | '' +> = '' extends M + ? string + : '+' extends M + ? readonly [string, ...string[]] + : '*' extends M + ? readonly string[] | undefined | null + : '?' extends M + ? string | undefined | null + : never + +/** + * Given a param name N and its modifier M, creates a param object for the pair. + * + * @internal + */ +export type _ParamToObject< + N extends string, + M extends _ParamModifier | '' +> = M extends '?' | '*' + ? { + [K in N]?: _ModifierParamValue + } + : { + [K in N]: _ModifierParamValue + } + +/** + * Takes the custom regex (and everything after) of a param and strips it off. + * + * @example + * - `\\d+(?:inner-group\\)-end)/:rest-of-url` -> `/:rest-of-url` + * + * @internal + */ +export type _RemoveUntilClosingPar = + S extends `${infer A}\\)${infer Rest}` + ? A extends `${string})${infer Rest2}` // the actual regexp finished before, AA has no escaped ) + ? Rest2 extends `${_ParamModifier}${infer Rest3}` + ? Rest2 extends `${infer M}${Rest3}` + ? `${M}}${Rest3}\\)${Rest}` + : never + : `}${Rest2}\\)${Rest}` // job done + : _RemoveUntilClosingPar // we keep removing + : S extends `${string})${infer Rest}` + ? Rest extends `${_ParamModifier}${infer Rest2}` + ? Rest extends `${infer M}${Rest2}` + ? `${M}}${Rest2}` + : never + : `}${Rest}` + : never // nothing to remove, should not have been called, easier to spot bugs + +/** + * Reformats a path string `/:id(custom-regex)/:other+` by wrapping params with + * `{}` and removing custom regexps to make them easier to parse. + * + * @internal + */ +export type _RemoveRegexpFromParam = + S extends `${infer A}:${infer P}${_ParamDelimiter}${infer Rest}` + ? P extends _ExtractFirstParamName

+ ? S extends `${A}:${P}${infer D}${Rest}` + ? D extends _ParamModifier | '' + ? `${A}{${P}${D}}${S extends `${A}:${P}${D}${infer Rest2}` // we need to infer again... + ? _RemoveRegexpFromParam + : never}` + : D extends _ParamDelimiter + ? '(' extends D + ? `${A}{${P}${S extends `${A}:${P}(${infer Rest2}` // we need to infer again to include D + ? _RemoveRegexpFromParam<_RemoveUntilClosingPar> + : '}'}` + : `${A}{${P}}${S extends `${A}:${P}${infer Rest2}` // we need to infer again to include D + ? _RemoveRegexpFromParam + : never}` + : never + : never + : never + : S extends `${infer A}:${infer P}` + ? P extends _ExtractFirstParamName

+ ? `${A}{${P}}` + : never + : S + +/** + * Extract the first param name (after a `:`) and ignores the rest. + * + * @internal + */ +export type _ExtractFirstParamName = + S extends `${infer P}${_ParamDelimiter}${string}` + ? _ExtractFirstParamName

+ : S extends `${string}${_ParamDelimiter}${string}` + ? never + : S + +/** + * Join an array of param values + * + * @internal + */ +type _JoinParams = V extends + | null + | undefined + ? '' + : V extends readonly [infer A, ...infer Rest] + ? A extends string + ? `${A}${Rest extends readonly [any, ...any[]] + ? `/${_JoinParams}` + : ''}` + : never + : '' + +/** + * Transform a param value to a string. + * + * @internal + */ +export type _ParamToString = V extends null | undefined | readonly string[] + ? _JoinParams + : V extends null | undefined | readonly never[] | readonly [] + ? '' + : V extends string + ? V + : `oops` + +/** + * Possible values for a Modifier. + * + * @internal + */ +type _PossibleModifierValue = + | string + | readonly string[] + | null + | undefined + | readonly never[] + +/** + * Recursively builds a path from a {param} based path + * + * @internal + */ +export type _BuildPath< + P extends string, + PO extends ParamsFromPath +> = P extends `${infer A}{${infer PP}}${infer Rest}` + ? PP extends `${infer N}${_ParamModifier}` + ? PO extends Record + ? PO[N] extends readonly [] | readonly never[] | null | undefined + ? `${A}${Rest extends `/${infer Rest2}` ? _BuildPath : ''}` + : `${A}${_ParamToString}${_BuildPath}` + : `${A}${Rest extends `/${infer Rest2}` ? _BuildPath : ''}` + : `${A}${PO extends Record + ? _ParamToString + : ''}${_BuildPath}` + : P + +/** + * Builds a path string type from a path definition and an object of params. + * @example + * ```ts + * type url = PathFromParams<'/users/:id', { id: 'posva' }> -> '/users/posva' + * ``` + */ +export type PathFromParams< + P extends string, + PO extends ParamsFromPath

+> = string extends P ? string : _BuildPath<_RemoveRegexpFromParam

, PO> + +/** + * A param in a url like `/users/:id`. + */ +export interface PathParserParamKey< + N extends string = string, + M extends _ParamModifier | '' = _ParamModifier | '' +> { + name: N + repeatable: M extends '+' | '*' ? true : false + optional: M extends '?' | '*' ? true : false +} + +/** + * Extracts the params of a path. + * + * @internal + */ +export type _ExtractPathParamKeys

= + P extends `${string}{${infer PP}}${infer Rest}` + ? [ + PP extends `${infer N}${_ParamModifier}` + ? PP extends `${N}${infer M}` + ? M extends _ParamModifier + ? PathParserParamKey + : never + : never + : PathParserParamKey, + ..._ExtractPathParamKeys + ] + : [] + +/** + * Extract the param keys (name and modifiers) tuple of a path. + */ +export type ParamKeysFromPath

= string extends P + ? readonly PathParserParamKey[] // Generic version + : _ExtractPathParamKeys<_RemoveRegexpFromParam

> diff --git a/test-dts/paths.test-d.ts b/test-dts/paths.test-d.ts new file mode 100644 index 00000000..0342cec2 --- /dev/null +++ b/test-dts/paths.test-d.ts @@ -0,0 +1,132 @@ +import type { + ParamsFromPath, + _ExtractFirstParamName, + _RemoveRegexpFromParam, + _RemoveUntilClosingPar, +} from './' +import { expectType } from './' + +function params(_path: T): ParamsFromPath { + return {} as any +} + +// simple +expectType<{}>(params('/static')) +expectType<{ id: string }>(params('/users/:id')) +// simulate a part of the string unknown at compilation time +expectType<{ id: string }>(params(`/${encodeURI('')}/:id`)) +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*')) + +// @ts-expect-error +expectType<{ other: string }>(params('/users/:id')) +// @ts-expect-error +expectType<{ other: string }>(params('/users/static')) + +// at beginning +expectType<{ id: string }>(params('/:id')) +expectType<{ id: readonly [string, ...string[]] }>(params('/:id+')) +expectType<{ id?: string | null | undefined }>(params('/:id?')) +expectType<{ id?: readonly string[] | null | undefined }>(params('/:id*')) + +// with trailing path +expectType<{ id: string }>(params('/users/:id-more')) +expectType<{ id: readonly [string, ...string[]] }>(params('/users/:id+-more')) +expectType<{ id?: string | null | undefined }>(params('/users/:id?-more')) +expectType<{ id?: readonly string[] | null | undefined }>( + params('/users/:id*-more') +) + +// multiple +expectType<{ id: string; b: string }>(params('/users/:id/:b')) +expectType<{ + id: readonly [string, ...string[]] + b: readonly [string, ...string[]] +}>(params('/users/:id+/:b+')) +expectType<{ id?: string | null | undefined; b?: string | null | undefined }>( + params('/users/:id?-:b?') +) +expectType<{ + id?: readonly string[] | null | undefined + b?: readonly string[] | null | undefined +}>(params('/users/:id*/:b*')) + +// custom regex +expectType<{ id: string }>(params('/users/:id(one)')) +expectType<{ id: string }>(params('/users/:id(\\d+)')) +expectType<{ id: readonly string[] }>(params('/users/:id(one)+')) +expectType<{ date: string }>(params('/users/:date(\\d{4}-\\d{2}-\\d{2})')) +expectType<{ a: string }>(params('/:a(pre-(?:\\d{0,5}\\)-end)')) + +// special characters +expectType<{ id$thing: string }>(params('/:id$thing')) +expectType<{ id: string }>(params('/:id&thing')) +expectType<{ id: string }>(params('/:id!thing')) +expectType<{ id: string }>(params('/:id\\*thing')) +expectType<{ id: string }>(params('/:id\\thing')) +expectType<{ id: string }>(params("/:id'thing")) +expectType<{ id: string }>(params('/:id,thing')) +expectType<{ id: string }>(params('/:id;thing')) +expectType<{ id: string }>(params('/:id=thing')) +expectType<{ id: string }>(params('/:id@thing')) +expectType<{ id: string }>(params('/:id[thing')) +expectType<{ id: string }>(params('/:id]thing')) + +function removeUntilClosingPar( + _s: S +): _RemoveUntilClosingPar { + return '' as any +} + +expectType<'}'>(removeUntilClosingPar(')')) +expectType<'+}'>(removeUntilClosingPar(')+')) +expectType<'}more'>(removeUntilClosingPar(')more')) +expectType<'}'>(removeUntilClosingPar('\\w+)')) +expectType<'}/more-url'>(removeUntilClosingPar('\\w+)/more-url')) +expectType<'}/:p'>(removeUntilClosingPar('\\w+)/:p')) +expectType<'+}'>(removeUntilClosingPar('oe)+')) +expectType<'}/:p(o)'>(removeUntilClosingPar('\\w+)/:p(o)')) +expectType<'}/:p(o)'>(removeUntilClosingPar('(?:no\\)?-end)/:p(o)')) +expectType<'}/:p(o(?:no\\)?-end)'>( + removeUntilClosingPar('-end)/:p(o(?:no\\)?-end)') +) +expectType<'}:new(eg)other'>(removeUntilClosingPar('customr):new(eg)other')) +expectType<'}:new(eg)+other'>(removeUntilClosingPar('customr):new(eg)+other')) +expectType<'}/:new(eg)+other'>(removeUntilClosingPar('customr)/:new(eg)+other')) +expectType<'?}/:new(eg)+other'>( + removeUntilClosingPar('customr)?/:new(eg)+other') +) +function removeRegexp(_s: S): _RemoveRegexpFromParam { + return '' as any +} + +expectType<'/{id?}/{b}'>(removeRegexp('/:id(aue(ee{2,3}\\))?/:b(hey)')) +expectType<'/{id+}/b'>(removeRegexp('/:id+/b')) +expectType<'/{id}'>(removeRegexp('/:id')) +expectType<'/{id+}'>(removeRegexp('/:id+')) +expectType<'+}'>(removeRegexp('+}')) +expectType<'/{id+}'>(removeRegexp('/:id(e)+')) +expectType<'/{id}/b'>(removeRegexp('/:id/b')) +expectType<'/{id}/{b}'>(removeRegexp('/:id/:b')) +expectType<'/users/{id}/{b}'>(removeRegexp('/users/:id/:b')) +expectType<'/{id?}/{b+}'>(removeRegexp('/:id?/:b+')) +expectType<'/{id?}/{b+}'>(removeRegexp('/:id(aue(ee{2,3}\\))?/:b+')) + +function extractParamName(_s: S): _ExtractFirstParamName { + return '' as any +} + +expectType<'id'>(extractParamName('id(aue(ee{2,3}\\))?/:b(hey)')) +expectType<'id'>(extractParamName('id(e)+:d(c)')) +expectType<'id'>(extractParamName('id(e)/:d(c)')) +expectType<'id'>(extractParamName('id:d')) +expectType<'id'>(extractParamName('id/:d')) +expectType<'id'>(extractParamName('id?/other/:d')) +expectType<'id'>(extractParamName('id/b')) +expectType<'id'>(extractParamName('id+')) +expectType<'id'>(extractParamName('id')) +expectType<'id'>(extractParamName('id-u')) +expectType<'id'>(extractParamName('id:u')) +expectType<'id'>(extractParamName('id(o(\\)e)o')) +expectType<'id'>(extractParamName('id(o(\\)e)?o'))