首页 / TypeScript 入门教程 / JSDoc 与 .d.ts 自动生成

TypeScript 入门教程

JSDoc 与 .d.ts 自动生成

本教程共 80 篇 · 第 66 篇 · 更新于 2026-08-10 · 约 14 分钟阅读

TypeScriptTypeScript 入门教程JSDoc.d.ts 生成allowJscheckJs渐进迁移

本节目标:学会两种”不用手写 .d.ts”的类型安全方案——用 JSDoc 注解给 JS 代码加类型,以及通过 tsc --declaration 自动从 TS 源码生成声明文件。你还将了解 allowJs + checkJs 混合项目的配置,以及从纯 JS 渐进迁移到 TypeScript 的路线。

问题:可以不写 .d.ts 吗

前五章我们都在手写声明文件——declare namespacedeclare module、module augmentation。这些技能在你用第三方 JS 库时必不可少。

但换个角度想:如果你的项目本身就是 TypeScript 写的,或者你有一个不打算改成 .ts 的 JS 项目但想加点类型安全——声明文件还有必要手写吗?

答案是不一定。TypeScript 提供了两条自动化路径:

  1. JSDoc 注解:在 .js 文件里通过注释标注类型,让 TS 编译器帮你检查。
  2. tsc --declaration:从 .ts 源码自动生成 .d.ts 声明文件,零手工维护。

JSDoc 类型注解入门

JSDoc 本来是为 JS 代码生成文档用的注释规范,TypeScript 把它”升级”成了类型标注工具。在 .js 文件里写 JSDoc 注释,TS 编译器能读懂并做类型检查。

最常用的几个标签:

@type —— 给变量指定类型

/** @type {number} */
let count;

count = 42;       // ✅
count = "hello";  // 如果开了 checkJs,这里会报错

@type 支持完整的 TypeScript 类型语法:

/** @type {string | number} */
let id;

/** @type {number[]} */
let scores;

/** @type {{ name: string; age: number }} */
let user;

/** @type {(a: number, b: number) => number} */
let add;

/** @type {Promise<string>} */
let promise;

@param 和 @returns —— 给函数标注类型

/**
 * 计算两点之间的距离
 * @param {number} x1 第一个点的 x 坐标
 * @param {number} y1 第一个点的 y 坐标
 * @param {number} x2 第二个点的 x 坐标
 * @param {number} y2 第二个点的 y 坐标
 * @returns {number} 两点间的欧氏距离
 */
function distance(x1, y1, x2, y2) {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

distance(0, 0, "3", 4); // checkJs 下会报错:第 3 个参数应为 number

@param 的语法是 @param {类型} 参数名 说明。类型可以是基础类型、联合类型、泛型,甚至复杂的 TS 类型语法。

@typedef —— 定义可复用的类型别名

/**
 * @typedef {Object} User
 * @property {number} id
 * @property {string} name
 * @property {string} [email]  方括号表示可选属性
 * @property {"admin" | "user"} role
 */

/**
 * @param {User} user
 * @returns {string}
 */
function getUserDisplay(user) {
  return `${user.name} (${user.role})`;
}

@typedef 定义的 User 类型可以在当前文件的其他 JSDoc 注释里复用。需要跨文件复用?用 @import

/** @typedef {import("./types").User} User */

@callback —— 定义回调函数类型

/**
 * @callback EventHandler
 * @param {string} eventName
 * @param {*} [data]
 * @returns {void}
 */

/** @type {EventHandler} */
let handler = (eventName, data) => {
  console.log(eventName, data);
};

@template —— 泛型

/**
 * @template T
 * @param {T} value
 * @returns {T}
 */
function identity(value) {
  return value;
}

identity("hello"); // 返回类型被推断为 string
identity(42);       // 返回类型被推断为 number

checkJs 与 allowJs

光写 JSDoc 注解还不够——你需要让 TS 编译器真正检查 .js 文件。

单文件启用:在 .js 文件顶部加一行注释:

// @ts-check

/** @type {number} */
let x = "hello"; // 报错:类型 "string" 不能赋给 "number"

全局启用:在 tsconfig.json 里设置:

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "strict": false,
    "noEmit": true
  },
  "include": ["src/**/*"]
}

两个配置项的区别:

  • allowJs: true:允许 TS 编译器处理 .js 文件(包括 import、类型推断)。
  • checkJs: true:对 .js 文件进行类型检查(依赖 allowJstrue)。

如果只开 allowJs 不开 checkJs,TS 会做类型推断但不报错——这意味着编辑器里你能看到类型提示和补全,但编译时不会因为类型错误而失败。这是一种非常低摩擦的过渡方案。

Tip

从纯 JS 迁移到 TS 的推荐顺序:先开 allowJs(获取智能提示)→ 再开 checkJs(开始出现类型警告)→ 逐步将文件改为 .ts 并开启 strict。每一步都可以单独进行,不会阻塞业务开发。

混合项目配置

一个典型的渐进迁移项目,tsconfig.json 大概长这样:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "allowJs": true,
    "checkJs": true,
    "strict": false,
    "noEmit": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["src/legacy/**/*"]
}

几个值得注意的点:

  • strict: false:新迁移的 JS 项目一开始开 strict 可能报几百个错。先关掉,逐个文件迁成 .ts 后再开。
  • noEmit: true:JS 文件不需要编译,只做类型检查。
  • exclude:旧代码/第三方脚本可以暂时排除出去,不影响新代码的类型安全。

tsc —declaration:自动生成 .d.ts

如果你的项目本身就是 TypeScript 写的,.d.ts 声明文件完全不用手写——tsc 可以自动生成。

tsconfig.json 里加上两个选项:

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./dist/types",
    "emitDeclarationOnly": true
  }
}
  • declaration: true:告诉 TS 为每个 .ts 文件生成对应的 .d.ts
  • declarationDir:生成的 .d.ts 放到哪个目录。不设置的话就跟 .js 输出放在一起。
  • emitDeclarationOnly: true:只生成 .d.ts 文件,不生成 .js。适合你用 Babel 或 esbuild 编译 JS、只需要 TS 出类型声明的场景(比如库开发)。

给定一个源文件:

// src/utils.ts
export function add(a: number, b: number): number {
  return a + b;
}

export interface Options {
  timeout: number;
  retries?: number;
}

export class TaskQueue {
  private queue: (() => Promise<void>)[] = [];

  enqueue(task: () => Promise<void>): void {
    this.queue.push(task);
  }
}

运行 tsc --declaration(或在 tsconfig.json 中配置后运行 tsc),生成的 utils.d.ts

// dist/types/utils.d.ts
export declare function add(a: number, b: number): number;

export interface Options {
    timeout: number;
    retries?: number;
}

export declare class TaskQueue {
    private queue;
    enqueue(task: () => Promise<void>): void;
}

注意生成的文件自动加了 declare 修饰符,且函数实现体被移除了(只剩签名)。private 成员 queue 的类型标注也被省略——因为对于外部使用者来说,它是不可见的。

package.json 的 types 字段

作为 npm 包发布时,你需要告诉使用者声明文件在哪:

{
  "name": "my-library",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "types": "./dist/types/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/types/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

使用 exports 字段可以更细粒度地控制不同入口的类型文件。对于多入口的包尤其有用。

JSDoc vs .d.ts:什么时候用哪个

场景推荐方案
你写 TS 源码,要发布 npm 包tsc --declaration 自动生成
你的项目是纯 JS,想渐进加类型JSDoc + allowJs + checkJs
你在用第三方 JS 库,它没有类型.d.ts(或找 @types
你要描述一个复杂的 API(重载、泛型等).d.ts(手写)比 JSDoc 更清晰
团队不打算用 TS,但想要类型提示JSDoc(零配置,编辑器就能读)

JSDoc 的优缺点:

  • 优点:不需要编译、不改变文件扩展名、团队零学习成本。
  • 缺点:语法臃肿(一个简单类型要写三四行注释)、不支持所有 TS 类型特性(比如复杂的条件类型)、维护起来不如 .ts 直观。

对于长期维护的项目,JSDoc 适合做过渡方案,最终目标还是迁移到 .ts

总结

本章覆盖了两种自动化类型方案:

  1. JSDoc 注解@type@param@returns@typedef@callback@template——给 .js 文件加类型标注,配合 // @ts-checkcheckJs: true 启用检查。
  2. tsc --declaration:从 .ts 源码自动生成 .d.ts 文件,适用于库开发和 TypeScript 项目。

声明文件系列到此结束。回顾六章的内容:从 .d.ts 的基本概念 → 全局库声明 → 模块库声明 → 插件与扩充 → @types 社区 → JSDoc 与自动生成。你已经掌握了”给 JS 代码加类型”的全套技能。

接下来,教程将进入 TypeScript 的工程化部分——tsconfig.json 编译选项的完整指南。