用TypeScript定义函数重载类型

91 阅读3分钟

请允许我用一个例子快速回答 "如何用TypeScript定义函数重载类型 "的 "正常 "使用情况。

我想要一个接受回调的函数,如果没有提供回调,则返回一个承诺:

const logResult = result => console.log(`result: ${result}`)
asyncAdd(1, 2).then(logResult) // logs "result: 3"
asyncAdd(3, 6, logResult) // logs "result: 9"

下面是你如何使用普通的JavaScript来实现这个API:

function asyncAdd(a, b, cb) {
  const result = a + b
  if (cb) return cb(result)
  else return Promise.resolve(result)
}

在这个实现中,我们想要的是让TypeScript抓住这个错误的用法:

// @ts-expect-error because when the cb is provided, void is returned so you can't use ".then"!
asyncAdd(1, 2, logResult).then(logResult) // this would throw an error when trying to use ".then" (except we're using TypeScript so it won't even compile 😉)

所以,这里是你如何输入这种重载的方式:

type asyncAddCb = (result: number) => void
// define all valid function signatures
function asyncAdd(a: number, b: number): Promise
function asyncAdd(a: number, b: number, cb: asyncAddCb): void

// define the actual implementation
// notice cb is optional
// also notice that the return type is inferred, but it could be specified as `void | Promise`
function asyncAdd(a: number, b: number, cb?: asyncAddCb) {
  const result = a + b
  if (cb) return cb(result)
  else return Promise.resolve(result)
}

然后你就可以去比赛了!

这篇博文的真正灵感是有点复杂的,需要一些背景信息。

我有一个包叫 babel-plugin-codegen的包,可以让你在编译时生成代码。例如,假设你有一个文件,有以下代码:

// @codegen
const fs = require('fs')
const fruits = fs.readFileSync('./fruit.txt', 'utf8').toString().split('\n')
module.exports = fruits
  .map(fruit => `export const ${fruit} = '${fruit}';`)
  .join('')

假设fruit.txt 包含一个水果的列表,这就是它的编译结果:

export const apple = 'apple'
export const orange = 'orange'
export const pear = 'pear'

所以,你生成了一串代码,而codegen将其转化为实际的代码,输入到你的输出中。这可以解锁很多非常酷的东西。

但这个babel插件的具体内容并不重要。重要的是,你也可以将其与 babel-plugin-macros这基本上是可导入的babel变换。因此,你可以不配置babel-plugin-codegen,而直接配置babel-plugin-macros ,然后安装 codegen.macro你就以像这样导入和使用它了。

import codegen from 'codegen.macro'

// using as a tagged template literal:
codegen`
  module.exports = "const tag = 'this is an example'"
`

// using as a function
codegen(`
  module.exports = "const fn = 'this is another example'"
`)

// codegen-ing an external module (and pass an argument):
const jpgs = codegen.require('./get-files-list', '**/*.jpg')

const ui = {`module.exports = require('./some-jsx-code')`}

然后,它可以编译成这样的东西:

// using as a tagged template literal:
const tag = 'this is an example'

// using as a function
const fn = 'this is another example'

// codegen-ing an external module (and pass an argument):
const jpgs = ['kody.jpg', 'olivia.jpg', 'marty.jpg']

const ui = This is some example JSX code

总之,codegen 是很不错的。但是你会注意到这里有一些核心的重载,所以我想我应该分享一下我是如何用TypeScript输入这个函数重载的。

需要记住的是,实际的codegen 函数实现实际上是一个babel宏,所以它看起来与这些函数的工作方式完全不同。它是在编译过程中被调用的,它被调用的参数是AST。

也就是说,到最后,消费者的体验才是最重要的,所以我们需要一个版本的codegen 函数,以预期的方式工作。所以我们要定义我们的类型,然后确保我们把宏函数像普通函数一样投递:

import {createMacro} from 'babel-plugin-macros'
import type {MacroHandler} from 'babel-plugin-macros'

const codegenMacro: MacroHandler = function codegenMacro(/* some args */) {
  // the implementation here is irrelevant
}

// use the `createMacro` utility to turn the codegenMacro into a babel macro
const macro = createMacro(codegenMacro)

好的,所以请记住,macro 函数实际上不曾被用户代码调用。这个函数将被babel-plugin-macros ,它将被调用的参数是MacroHandler 。然而,就TypeScript而言,开发者调用它,所以我们需要给它正确的类型定义,大家都会很高兴。所以让我们来定义这些。

// This handles the tagged template literal API:
declare function codegen(
  literals: TemplateStringsArray,
  ...interpolations: Array
): any

// this handles the function call API:
declare function codegen(code: string): any

// this handles the `codegen.require` API:
declare namespace codegen {
  function require(modulePath: string, ...args: Array): any
}

// Unfortunately I couldn't figure out how to add TS support for the JSX form
// Something about the overload not being supported because codegen can't be all the things or whatever
// PRs welcome!

有了这些重载的定义,现在我们只需要强迫TypeScript把我们的macro 文件当作我们定义的codegen 函数。我们还需要使其成为我们的宏文件的默认输出,所以我们将一次完成所有这些。

export default macro as typeof codegen

你可以在babel-plugin-codegensrc/macro.ts 文件中一起浏览它。

我希望这很有用!祝你们好运!