第14章:JavaScript 渐进类型化 —— JSDoc、检查与声明生成
约 3 分钟 · 更新于 2026-09-01
第14章:JavaScript 渐进类型化 —— JSDoc、检查与声明生成
TypeScript 不要求一次把所有 .js 改成 .ts。本章建立渐进路线:先让 JavaScript 获得类型检查,再用 JSDoc 描述契约,最后按价值迁移文件或生成声明。
一、让 JavaScript 进入 TypeScript 项目
json
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": true
},
"include": ["src/**/*"]
}
也可只在单个文件顶部启用:
临时关闭单文件检查:
后者应极少使用并记录原因。
二、JavaScript 中的推断规则
TypeScript 会根据赋值、构造函数和 CommonJS 模式推断:
js
class User {
constructor(name) {
this.name = name;
this.active = true;
}
}
对象字面量在 JS 检查中可能比 TS 文件更开放;函数参数默认可选等行为也可能更宽松。迁移时不要假设 .js 与 .ts 的检查严格度完全相同。
三、JSDoc 基础
js
/** @type {string} */
let route = "HKG-NRT";
/**
* @param {number} left
* @param {number} right
* @returns {number}
*/
function add(left, right) {
return left + right;
}
四、定义可复用类型
js
/**
* @typedef {Object} User
* @property {string} id
* @property {string} name
* @property {boolean} [active]
*/
js
/** @type {User} */
const user = { id: "1", name: "Ada" };
回调类型:
js
/** @callback Formatter
* @param {number} value
* @returns {string}
*/
五、泛型 JSDoc
js
/**
* @template T
* @param {T[]} values
* @returns {T | undefined}
*/
function first(values) {
return values[0];
}
约束和默认参数也可表达,但复杂度上升后 .ts 通常更清晰。
六、导入类型
js
/** @typedef {import("./types").User} User */
或者在类型位置直接使用:
js
/** @param {import("./types").User} user */
function save(user) {}
这样不会产生运行时导入。
七、JSDoc 中的 @satisfies
js
/** @satisfies {Record<string, string>} */
const routes = {
home: "/",
user: "/users/:id"
};
它检查约束,同时保留对象自身精确类型,作用类似 TypeScript 的 satisfies。
八、类相关标签
常用标签:
- @implements
- @extends
- @constructor
- @this
- @readonly
- @override
js
/** @implements {Serializable} */
class UserRecord {
serialize() {
return "{}";
}
}
JSDoc 只能描述,仍需真实实现满足运行时行为。
九、从宽松到严格的迁移顺序
- 开启 allowJs。
- 仅对关键目录启用 checkJs。
- 修复明显拼写和参数错误。
- 为公共函数、数据边界和复杂对象补 JSDoc。
- 将 any 改为 unknown 并增加守卫。
- 迁移叶子模块为 .ts。
- 逐步开启 noImplicitAny、严格空值等规则。
- 最后迁移入口、框架胶水和高耦合模块。
十、常见迁移错误
顺序添加属性
js
const options = {};
options.timeout = 3000;
改成一次性对象字面量或 JSDoc 类型,便于推断。
参数数量不一致
JavaScript 调用可多传少传,TypeScript 会按声明检查。应确认真实 API,而不是通过可选参数掩盖错误。
Object、{} 与 object
- Object:通常过宽,不推荐。
- {}:除 null/undefined 外几乎所有值。
- object:非原始值。
- Record<string, unknown>:字符串键记录,但仍有具体假设。
十一、生成 .d.ts
成熟 JS 库可在保留 JavaScript 实现的同时输出声明:
json
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist/types"
}
}
生成声明前应确保公共函数有充分 JSDoc,否则会产生 any 或过宽类型。
十二、抑制指令治理
js
// @ts-expect-error 第三方声明缺陷,升级 SDK 后删除
legacyApi.callUnsupportedShape();
@ts-expect-error 会在错误消失时提醒,优于 @ts-ignore。CI 可统计抑制指令并禁止无说明新增。
十三、何时不必迁移为 .ts
保留 .js + JSDoc 可能适合:
- 向纯 JavaScript 用户发布的库。
- 配置文件和构建脚本。
- 团队需要渐进采用。
- 运行环境直接消费 JS,不希望增加转译步骤。
目标是获得足够可靠的反馈,不是追求扩展名统一。
十四、练习
- 给一个 JS 工具文件添加 @ts-check。
- 用 @typedef 建模用户对象。
- 用 @template 实现泛型 first。
- 生成声明文件并检查是否出现意外 any。
- 制定一份按目录推进的迁移清单。
十五、总结
- TypeScript 可直接检查 JavaScript,不必一次性重写。
- JSDoc 能表达参数、返回值、对象、泛型和类契约。
- JS 文件的推断规则比 TS 更宽松,迁移时要理解差异。
- 渐进迁移应从边界和叶子模块开始。
- .d.ts 生成结果仍需审查和消费测试。
请继续阅读:第15章:浏览器开发。
原始资料引用