Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lmk123 opened this issue Jul 5, 2023 · 1 comment
Open

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

lmk123 opened this issue Jul 5, 2023 · 1 comment

Comments

@lmk123
Copy link
Owner

lmk123 commented Jul 5, 2023

背景

在开发 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
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants