blog
blog copied to clipboard
关于一个函数的 TypeScript 类型标注问题
背景
在开发 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 类型了。点击这里查看
hello,有个问题想请教您,方便联系您一下吗?