Typescript中如何快速的生成多个函数重载的效果?

标题可能不太正确,大致效果如下

// 每个 url 都对应着一个 params,怎么写比较优雅,有较好的提示性 
function get(url: '/login', params: {username:string, password: string})
function get(url: '/logout')
function get(url: '/changePassword', params: {username: string, oldPwd: string, newPwd:string})
function get(url:string, params?:any){
  // .... 
  console.log(`${url}::${JSON.stringify(params)}`)
}

ts中有没有存在一种类型,或者说特殊的写法,可以将 urlparams类型进行映射?
比如说

// 模拟代码
typeObj = {'/login': {username:string, password: string}}
//在function的声明中利用 keyOf in 之类的方式依次取出,就能实现重载的效果?

提供思路也可以,当然最好是直接给个demo, 十分感谢!

阅读 2.7k
3 个回答

简单版本

如果只是为了声明方便的话,只要使用以下语句即可,而且使用get函数的时候,能根据传入的url值,去推断params的值

type LoginParam = { username: string; password: string }
type ChangePasswordParam = { username: string; oldPwd: string; newPwd: string }
type TypeObj = {
  '/login': LoginParam
  '/changePassword': ChangePasswordParam
}

function get<K extends keyof TypeObj = keyof TypeObj>(
  url: K,
  params: TypeObj[K],
) {}

const loginParam = {
  username: 'user',
  password: 'pass',
}
get('/login', loginParam)

// 这里会报错
get('/changePassword', loginParam)

Playground

更近一步

对于使用get的时候,TS 已经足够聪明了,不过对于在get内部去使用时,TS 还不够聪明.比如下面的情况,就会报错,提示ChangePasswordParam类型中不存在password,这就很反直觉.如果要使用password,需要通过类型断言,将params指定为一个更具体的类型LoginParam,才能使用password.

function get<K extends keyof TypeObj = keyof TypeObj>(
  url: K,
  params: TypeObj[K],
) {
  if (url === '/login') {
    // 这里会报错,提示`ChangePasswordParam`类型中不存在`password`
    params.password
    // 通过类型断言指定一个更具体的类型
    ;(params as LoginParam).password
  }
}

Playground

不过这样看着就很别扭,根据上下文,明明在url === '/login'就已经可以判定params的类型,却还得手动在加一层断言,而且这样手动加的情况,是最容易出问题的,比如下面这样,明显是错的,但是 TS 就是不报错.这仅仅只有一行还好,我们一眼就能看出来了,但是如果代码一多,我估计就不是很容易发现了.

function get<K extends keyof TypeObj = keyof TypeObj>(
  url: K,
  params: TypeObj[K],
) {
  if (url === '/login') {
    // 通过类型断言指定一个更具体的类型
    ;(params as ChangePasswordParam).oldPwd
  }
}

其实还是有办法能解决的,只是需要稍微变通一下,我们看看下面的例子

type ObjType = { type: 'a'; a: 1 } | { type: 'b'; b: 1 }

function foo(obj: ObjType) {
  if (obj.type === 'a') {
    obj.a
  }
}

这种情况下,TS 是正确推断出来 obj 的类型的.而如果把 ObjType 换成数组(元组)的联合类型,也依然能生效,如下

type ObjType = ['a', { a: 1 }] | ['b', { b: 1 }]

function foo(obj: ObjType) {
  if (obj[0] === 'a') {
    obj[1].a
  }
}

那我们的参数列表,其实就是一个数组结构,只要改变一下写法,使用 ES6 的 Rest 参数,就能获得跟上面一样的效果

type ArgType =
  | ['/login', { username: string; password: string }]
  | ['/changePassword', { username: string; oldPwd: string; newPwd: string }]

function foo(...args: ArgType) {
  if (args[0] === '/login') {
    const [url, param] = args
    param.password
  }
}

最终的版本

type TypeObj = {
  '/login': { username: string; password: string }
  '/changePassword': { username: string; oldPwd: string; newPwd: string }
}

type GenerateArgTypeFromObject<T> = {
  [K in keyof T]: [url: K, params: T[K]]
}[keyof T]

function get(...args: GenerateArgTypeFromObject<TypeObj>) {
  if (args[0] === '/login') {
    const [url, param] = args
    param.password
  }
}

Playground

当然有呀,不需要重载,你看内置的 addEventListenr 这个方法是不是就是你要的效果?

照着它的声明去实现就好了。

image.png

image.png

image.png

type Obj = {'/login': {username:string, password: string}}

function get(url: K extends keyof Obj, params: Obj[K])

试试?