Item 70: Mirror Types to Sever Dependencies
要点
- Avoid transitive type dependencies in published npm modules.
- Use structural typing to sever dependencies that are nonessential.
- Don't force JavaScript users to depend on
@types
. Don't force web developers to depend on Node.js. - 避免在发布的 npm 模块中出现传递类型依赖。
- 使用结构化类型来切断非必要的依赖。
- 不要强迫 JavaScript 用户依赖
@types
,也不要强迫 web 开发者依赖 Node.js。
正文
假设你编写了一个用于解析 CSV 文件的库。它的 API 很简单:你传入 CSV 文件的内容,然后得到一个将列名映射到值的对象列表。
为了方便你的 Node.js 用户,你允许内容既可以是字符串,也可以是 Node.js Buffer:
// parse-csv.ts
import { Buffer } from 'node:buffer'
function parseCSV(contents: string | Buffer): { [column: string]: string }[] {
if (typeof contents === 'object') {
// It's a buffer
return parseCSV(contents.toString('utf8'))
}
// ...
}
Buffer 的类型定义来自 Node.js 类型声明,你必须安装它:
npm install --save-dev @types/node
这里我们遵循了第 65 条的建议,将 @types
作为开发依赖而不是生产依赖。
当你发布你的 CSV 解析库时,你使用 --declaration
生成类型声明并将其打包。生成的 .d.ts
文件如下所示:
// parse-csv.d.ts
import { Buffer } from 'node:buffer'
export declare function parseCSV(contents: string | Buffer): {
[column: string]: string
}[]
如果你采用这种方法,你的库的 JavaScript 用户会很高兴,但 TypeScript web 开发者不会。你会收到他们的投诉,说他们从你的库中得到了错误:
Cannot find module 'node:buffer' or its corresponding type declarations.
因为我们将 @types/node
作为 devDependency,所以它不会随我们的包一起安装,即使我们的类型(作为我们包的一部分)依赖于它。
那么我们应该将 @types/node
作为生产依赖吗?这会让错误消失,但现在你可能会收到另一组投诉:
- JavaScript 开发者会想知道这些他们依赖的
@types
模块是什么。 - TypeScript web 开发者会想知道为什么他们要依赖 Node.js。
- 使用不同版本 Node.js 的 TypeScript 开发者会想知道为什么他们有重复的类型定义。
这些投诉是合理的。Buffer 行为不是必需的,只对已经在使用 Node.js 的用户相关。而 @types/node
中的声明只对同时使用 TypeScript 的 Node.js 用户相关。@types/node
包不小(近 10 万行代码),我们的库只使用了其中很小的一部分。
TypeScript 的结构化类型(第 4 条)可以帮助你摆脱困境。与其使用来自 @types/node
的 Buffer 声明,你可以只写你需要的方法和属性。在这种情况下,只是一个可以接受编码的 toString 方法:
export interface CsvBuffer {
toString(encoding?: string): string
}
export function parseCSV(
contents: string | CsvBuffer
): { [column: string]: string }[] {
// ...
}
这个接口比完整的接口短得多,但它确实捕获了我们对 Buffer 的(简单)需求。在 Node.js 项目中,用真实的 Buffer 调用 parseCSV 仍然可以,因为类型是兼容的:
parseCSV(new Buffer('column1,column2\nval1,val2', 'utf-8')) // OK
再次查看 CsvBuffer 接口,它没有任何特定于 CSV 文件的内容。给它一个更"结构化"的名称可以强化这一点:
/** Anything convertible to a string with an encoding, e.g. a Node buffer. */
export interface StringEncodable {
toString(encoding?: string): string
}
import { Buffer } from 'node:buffer'
import { parseCSV } from './parse-csv'
test('parse CSV in a buffer', () => {
expect(parseCSV(new Buffer('column1,column2\nval1,val2', 'utf-8'))).toEqual([
{ column1: 'val1', column2: 'val2' },
])
})
这个测试验证了你的代码的运行时行为和 Node Buffer 对 StringEncodable 的可赋值性。测试导入了 node:buffer
,但这没关系,因为 @types/node
可以是 devDependency 而不影响你的库的用户。
如果你的代码开始使用 Buffer 接口的更多方法,那么你还需要将它们添加到你的接口版本中。这可能感觉重复,但正如 Go 语言社区所说,"一点复制比一点依赖更好"。如果你依赖另一个库类型的大部分,你可以选择通过供应商化依赖来正式化这种复制。
无论如何,通过切断 @types
依赖,你为 JavaScript 和各种 TypeScript 开发者提供了良好的体验。如果 @types
依赖有自己的依赖,那么你可能会切断整个依赖树,这对编译器性能有巨大的积极影响(第 78 条)。
这种技术也有助于切断单元测试和生产系统之间的依赖。参见第 4 条中的 getAuthors 示例。
要点回顾
- 避免在发布的 npm 模块中出现传递类型依赖。
- 使用结构化类型来切断非必要的依赖。
- 不要强迫 JavaScript 用户依赖
@types
,也不要强迫 web 开发者依赖 Node.js。