lmk123 / blog

个人技术博客,博文写在 Issues 里。
https://github.com/lmk123/blog/issues
623 stars 35 forks source link

关于一个函数的 TypeScript 类型标注问题 #126

Open lmk123 opened 10 months ago

lmk123 commented 10 months ago

背景

在开发 Chrome 扩展程序的时候,大部分 chrome.* 接口都是类似于下面这样的 callback 风格:

chrome.tabs.create({ url: 'https://hcfy.app' }, tab => {
  console.log(tab.id)
})

新出的 Manifest V3 同时支持 callback 风格与 Promise 风格:

// Manifest V3 中的 Promise 风格
const tab = await chrome.tabs.create({ url: 'https://hcfy.app' })
console.log(tab.id)

划词翻译目前仍然停留在 Manifest V2,能且只能用 callback 写法,但我又想要用 Promise,怎么办?

解决方案

几年前,我曾开发过一个项目叫 chrome-call,它通过如下方式使用:

import chromeCall from 'chrome-call'
const tab = await chromeCall(chrome.tabs, 'create', { url: 'https://hcfy.app' })

现在,我更喜欢 Node.js 中 util.promisify() 的形式,而 chrome 版本的 promisify() 函数很快就写好了:

function chromePromisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn.call(null, ...args, (result) => {
        const error = chrome.runtime.lastError
        if (error) {
          reject(error)
        } else {
          resolve(result)
        }
      })
    })
  }
}

const tabsCreate = chromePromisify(chrome.tabs.create)
const tab = await tabsCreate({ url: 'https://hcfy.app' })

但是接下来,在给这个函数添加 TypeScript 类型注解的时候,我犯了难。

第一个版本的类型注解

function chromePromisify(fn: (...args: any[]) => void) {
  return function (...args: any[]) {
    return new Promise((resolve, reject) => {
      fn.call(null, ...args, (result: unknown) => {
        const error = chrome.runtime.lastError
        if (error) {
          reject(error)
        } else {
          resolve(result)
        }
      })
    })
  }
}

这样确实能用,但会丢掉 @types/chrome 里对 chrome.tabs.create 函数的类型提示:

// 直接使用时,TypeScript 能检测参数的类型
chrome.tabs.create({ url: 'https://hcfy.app' }, tab => {
  // 并且能推断出 tab 的类型是 chrome.Tabs.tab
  console.log(tab.id)
})

// 用了 chromePromisify 之后
const tabsCreate = chromePromisify(chrome.tabs.create)
// 参数是 any[] 所以填什么都不会报类型错,返回值类型是 unknow 所以必须自行声明类型
const tab: chrome.Tabs.tab = await tabsCreate({ url: 'https://hcfy.app' })

第二个版本的类型注解

我希望这个函数能更加智能,所以参考了 @types/node 中对 util.promisify() 的类型注解,写出了第二个版本:

function chromePromisify<TResult>(fn: (callback: (result: TResult) => void) => void): () => Promise<TResult>
function chromePromisify<T1, TResult>(fn: (arg1: T1, callback: (result: TResult) => void) => void): (arg1: T1) => Promise<TResult>
// 后面就是不断增加 T1、T2、T3……
function chromePromisify(fn: Function): Function {
  return function (...args: any[]) {
    return new Promise((resolve, reject) => {
      fn.call(null, ...args, (result: unknown) => {
        const error = chrome.runtime.lastError
        if (error) {
          reject(error)
        } else {
          resolve(result)
        }
      })
    })
  }
}

在用 chrome.tabs.create 方法做测试时,这个版本很好的达成了我的预期,点击此链接可以看到 TypeScript 成功在参数类型错误时报了错,且能正确推断出返回值的类型。

但是在实际使用中,我发现它对于有多个重载的函数不起作用,比如 chrome.windows.get,点击此链接可以看到具体情况。

第三个版本

我想更进一步,让它支持有多个重载的函数。我猜测应该可以用 infer 关键字提取出函数的参数类型,于是搜了一下,然后就被密密麻麻的代码劝退了:https://stackoverflow.com/a/74209026

不过我还是想优化一下第二个版本那种不断重复添加 T1、T2、T3 的写法。我想要的类型是最后一个参数是 callback 的函数,而我不想通过重复给它添加 T1、T2、T3 的重载来获取到正确的类型。

在经过一番搜索之后,我在这里查到了下面这种写法:

type Bar = any
type Qux = number
// 前面几个都是 Bar 类型,但最后一个是 Qux 的 tuple
type WithLastQux = [...Bar[], Qux]

这种形式不正好能满足我想要的“最后一个参数是 callback 的函数”的类型吗:

type FnWithLastCallback = (...args: [...unknow[], (result: unknow) => void]) => void

上面这个类型可以再进一步,提取出参数列表与函数结果:

type FnWithLastCallback<TArgs extends Array<unknow>, TResult> = (...args: [...TArgs, (result: TResult) => void]) => void

然后,我们用这个类型来改造上面的 chromePromisify

function chromePromisify<TArgs extends Array<unknown>,TResult>(fn: (...args:[...TArgs, (result:TResult)=>void])=>void) {
  return function (...args: TArgs) {
    return new Promise<TResult>((resolve, reject) => {
      fn.call(null, ...args, (result: TResult) => {
        const error = chrome.runtime.lastError
        if (error) {
          reject(error)
        } else {
          resolve(result)
        }
      })
    })
  }
}

这样,就不用重复声明 T1、T2、T3 类型了。点击这里查看

letscubo commented 10 months ago

hello,有个问题想请教您,方便联系您一下吗?