当类型从不时使参数可选

Make parameter optional when type is never

我想写一个给定路由和一组可选参数的函数 returns url 路由中的占位符替换为给定的参数。
参数的类型应该与路由中的占位符匹配,如果路由没有占位符,则参数应该是可选的。这是预期结果的示例:

type Route = '/no-placeholders' | '/path/:p1/:p2'

buildUrl('/unknown-route') // error, route does not exist
buildUrl('/no-placeholders', {}) // error, expected 1 param got 2
buildUrl('/no-placeholders')
buildUrl('/path/:p1/:p2') // error, expected 2 params, got 1
buildUrl('/path/:p1/:p2', { foo: '' }) // error, { foo: string } does not match { p1: string; p2: string }
buildUrl('/path/:p1/:p2', { p1: 'foo', p2: 'bar' })

所以,我得出的结果如下,重载在这里没有用,但我试图以不同的方式处理 never 的情况:

type PathParams<Path extends string> =
    Path extends `:${infer Param}/${infer Rest}` ? { [k in Param | keyof PathParams<Rest>]: string } :
    Path extends `:${infer Param}` ? { [k in Param]: string } :
    Path extends `${infer _Prefix}:${infer Rest}` ? { [k in keyof PathParams<`:${Rest}`>]: string } :
    never;

type Route = '/no-placeholders' | '/path/:p1/:p2'

function buildUrl<R extends Route>(route: R, params: never): string;
function buildUrl<R extends Route>(route: R, params: PathParams<R>): string;
function buildUrl(route: any, params: any) {
  // ...
  return route
}


buildUrl('/unknown-route') // expect error
buildUrl('/no-placeholders', {}) // expect error
buildUrl('/no-placeholders')
buildUrl('/path/:p1/:p2') // expect error
buildUrl('/path/:p1/:p2', { foo: '' }) // expect error
buildUrl('/path/:p1/:p2', { p1: 'foo', p2: 'bar' })

问题是 buildUrl 总是需要传递第二个参数,即使给定的路由没有占位符。当 PathParams<R> returns never?

时,有没有办法让第二个参数可选?

解决方案

在重载中显式处理没有占位符的路由:

type RouteWithoutParams = '/no-placeholders'
type RouteWithParams = '/path/:p1/:p2'
type Route = RouteWithoutParams | RouteWithParams

function buildUrl<R extends RouteWithoutParams>(route: R): string;
function buildUrl<R extends Route>(route: R, params: PathParams<R>): string;
function buildUrl<R extends string>(route: R, params?: PathParams<R>) {
  // ...
  return route
}

如果你想处理丢失的参数,你需要明确地将其标记为可选:

type PathParams<Path extends string> =
  Path extends `:${infer Param}/${infer Rest}` ? { [k in Param | keyof PathParams<Rest>]: string } :
  Path extends `:${infer Param}` ? { [k in Param]: string } :
  Path extends `${infer _Prefix}:${infer Rest}` ? { [k in keyof PathParams<`:${Rest}`>]: string } :
  never;

type Route = '/no-placeholders' | '/path/:p1/:p2'

function buildUrl<R extends '/no-placeholders'>(route: R): string;
function buildUrl<R extends Route>(route: R, params: PathParams<R>): string;
function buildUrl<R extends string>(route: R, params?: PathParams<R>) {
  // ...
  return route
}


buildUrl('/unknown-route') // error, route does not exist
buildUrl('/no-placeholders', {}) // error, expected 1 param got 2
buildUrl('/path/:p1/:p2') // error, expected 2 params, got 1
buildUrl('/path/:p1/:p2', { foo: '' }) // error, { foo: string } does not match { p1: string; p2: string }


buildUrl('/no-placeholders') // ok
buildUrl('/path/:p1/:p2', { p1: 'foo', p2: 'bar' }) // ok

Playground

您需要为文字 '/no-placeholders'

显式定义重载规则