作为一个在 TypeScript 和 JavaScript 项目上都有丰富经验的开发者,我个人的偏好更倾向于后者。
这并非是因为我不喜欢为变量或函数定义类型。恰恰相反,我对此非常推崇,甚至在编写 Ruby 代码时也会保持这个习惯。
但问题的核心在于,我不认为“类型”应该成为代码本身的一部分。在我看来,代码应当只关注其行为逻辑——它是什么,以及它做什么。而那些描述代码的“元信息”,例如“这是一个字符串”或“那是一个整数”,其实更适合作为文档的一部分,以注释的形式存在。毕竟,为代码编写文档本就是应有之义。那种“不要在代码中写太多注释”的说法,实在是一种误导。
我坚信,你应该尽可能地为函数、值对象、类和各种结构添加注释(当然,要简洁明了,通常一两句话就足够了)。这就自然而然地引出了我们今天要讨论的工具——JSDoc。
在为 JavaScript 编写文档时,JSDoc 是首选标准。即便你从未用它来生成 API 文档网站,JSDoc 注释也能被众多现代开发工具和编辑器识别并解析。说到这里,就不得不提到 TypeScript。
等等,你不是更喜欢 JavaScript 吗?怎么又聊起 TypeScript了?
原因在于,即使你编写的是纯 JavaScript 文件,只要配合使用 JSDoc,TypeScript 引擎同样能为你提供强大的类型检查功能。这听起来可能有点绕,但效果却非常实在。
来看一个例子。在标准的 TypeScript 中,声明一个字符串变量通常这样写:
let str: string
str = “Hello world”
str = 123 // 这会引发类型错误!
然而,在纯 JavaScript (.js) 文件中,你只需在文件开头添加 // @ts-check,并用 JSDoc 标注类型,就能获得完全相同的类型检查效果:
// @ts-check
/** @type {string} */
let str
str = “Hello world”
str = 123 // 这同样会报类型错误!
如果你使用的是 VSCode 或 Zed 这类编辑器,类型提示和错误通常会自动显示。不过,我建议你通过 npm install typescript -D 在项目中安装 TypeScript。这样,你就能在 CI/CD 流程中独立运行 TypeScript 编译器 (tsc) 来执行类型检查或生成类型声明文件。
在我的 package.json 中,通常会配置这样一个脚本:
“build:types”: “npx tsc”
你可以通过在项目根目录创建 jsconfig.json 文件来配置类型检查的行为,例如:
{
“compilerOptions”: {
“strictNullChecks”: false,
“target”: “es2022”
}
}
同时,你可能还需要创建一个 tsconfig.json 文件,其配置示例如下:
{
// 根据你的项目结构调整 `include` 路径
“include”: [“src/**/*”],
“compilerOptions”: {
// 允许读取 JS 文件,默认会忽略它们
“allowJs”: true,
// 生成 .d.ts 声明文件
“declaration”: true,
// 仅生成声明文件,不输出 JS
“emitDeclarationOnly”: true,
// 声明文件输出目录
“outDir”: “types”,
// 在编辑器中“转到定义”时,跳转到 JS 源文件
“declarationMap”: true
}
}
我知道初看之下配置项有点多,但请放心,一旦你的编辑器和命令行工具配置妥当,这套流程可以轻松复用到无数项目中,你会很快上手。
理论不如实践,让我们直接看看具体的代码示例。
JSDoc 实战
以下是一个包含多个参数的类构造函数的注释示例:
class ReciprocalProperty {
/**
* @param {HTMLElement} element - 要绑定的 DOM 元素
* @param {string} name - 属性名称
* @param {(value: any) => any} signalFunction - 用于创建信号的函数
* @param {() => any} effectFunction - 用于建立副作用的函数
*/
constructor(element, name, signalFunction, effectFunction) {
this.element = element
this.name = name
this.type = this.determineType()
// 其他初始化逻辑...
}
}
如你所见,当你对变量的具体类型不那么关心时,使用 any 类型完全没有问题。将类型信息写入注释的一大额外好处是:你不仅获得了类型提示,还顺便完成了代码文档的编写!
再比如,声明一个对象类型(在 TypeScript 中常称为 Record):
/** @type {Record<string, ReciprocalProperty>} */
const attrProps = this.element[“_reciprocalProperties”]
这里使用 this.element[“_reciprocalProperties”] 而非点号语法,是因为 TypeScript 对访问对象上未声明的属性会发出警告。使用方括号表示法可以避免这个错误(前提是你确信这个属性存在)。
下面是在 for…of 循环中内联声明类型的例子:
for (const stop of /** @type {StreetcarStatementElement[]} */ ([…this.children])) {
stop.operate()
}
这种语法确实需要一点时间来适应。通常的规则是,当你需要在一段表达式前进行内联类型声明时,可以将 /** @type */ 注释放在一个用括号包裹的代码段之前,这通常都能奏效。
另一个例子是在函数参数中进行内联类型声明:
htmx.defineExtension(“streetcar”, {
handleSwap: (swapStyle, _target, /** @type {Element} */ fragment) => {
// 处理逻辑...
},
})
你还可以通过 @typedef 语法来“导入”仅用于类型的定义(注意,这不是普通的 JavaScript 导入):
/**
* @typedef { import(“./HostEffects.js”).default } HostEffects
*/
// 随后即可使用:
/**
* @param {HostEffects} effects
*/
function setup(effects) { }
@typedef 也可用于定义相当于 TypeScript 中 interface 的结构,JSDoc 官方文档对此有详细说明。
最后,有时你可能会编写一些 TypeScript 无法理解或报错的代码,这也没关系。正如官方文档所说:
当你认为 TypeScript 给出的错误不合理时,可以在该行代码前一行添加 // @ts-ignore 或 // @ts-expect-error 来忽略该特定错误。
为何 JavaScript + JSDoc + tsc 是更佳组合
我曾多次表达过一个观点:将 TypeScript 作为行业默认选择,可能是一个不小的误区。我真诚地希望更多开发者能开始编写真正符合开放 Web 标准的 .js 文件——这些文件无需任何构建步骤或转译工具,就能在浏览器或 Node.js 环境中直接运行。
与此同时,通过结合使用 JSDoc 和 tsc,你可以在 IDE 中获得智能的类型提示,在持续集成流水线中进行严格的类型检查,从而享受到与使用 TypeScript 几乎相同的开发体验。这种方式真正实现了“鱼与熊掌兼得”。而真正必须使用 .ts 文件的场景,在实践中少之又少。
当然,某些前端框架或工具链可能“强制”要求使用 TypeScript 编写代码。如果遇到这种情况,遵循框架约定即可。
但如果你对项目的技术选型拥有决策权,我强烈建议你考虑使用纯粹的 JavaScript。
毕竟,ECMAScript (JavaScript) 才是 Web 世界的通用语言,而非任何其超集或变体。通过在代码中编写清晰的 JSDoc 注释并利用 TypeScript 编译器进行静态验证,你可以在保持代码纯净、可移植性的同时,获得强大的类型安全保障。
希望这篇实践指南能为你提供一种新的视角。欢迎在 云栈社区 分享你在实际项目中使用 JSDoc 和类型检查的心得与技巧。
关于本文