本文详解 TypeScript 声明文件的两种主要捆绑方式:一是将 .d.ts 文件直接集成在 npm 包中,通过 package.json 的 types 字段自动加载;二是利用 @types 组织发布第三方库的类型定义。通过具体代码示例展示了从项目初始化、配置 tsconfig.json 到模拟发布库导入的完整流程,帮助开发者解决 TypeScript 引入 JavaScript 库时的类型报错问题。
TypeScript 与声明文件概述
前言
TypeScript 被定义为 JavaScript 类型的超集,这不仅是官方文档的描述,也揭示了两者之间紧密的联系。
从本质上看,TypeScript 在 JavaScript 的基础上引入了强类型语言的特性。开发者使用 TypeScript 语法进行编码,最终通过编译工具将其转换为标准的 JavaScript 代码。
使用 TypeScript 可以有效避免在原生 JavaScript 开发中因弱类型特性带来的潜在问题,例如不清楚函数输入参数或返回值类型等困惑。
虽然强类型的 JavaScript 开发体验良好,但许多开发者仍希望保留 npm 生态中丰富的第三方库支持。
目前,许多主流库已原生支持 TypeScript。即使部分库未主动适配,也有社区开发者通过贡献类型定义文件来提供支持。
这引出了本文的核心主题:TypeScript 声明文件。可以将其类比为 C 语言中的头文件,其核心作用是帮助 TypeScript 识别并引入 JavaScript 库的类型信息。
那么,究竟什么是声明文件?
它与 C/C++ 中的头文件(Header files)概念相似。在 C/C++ 中,当引用第三方库时,编译器无法自动识别导出的名称和函数签名,必须依赖头文件进行接口声明。
同理,TypeScript 的声明文件是以 .d.ts 为后缀的代码文件,用于描述一个 JavaScript 模块(广义上)内所有导出接口的类型信息。
关于声明文件的具体写法和规范,可参考以下官方文档及优质文章:
- https://www.tslang.cn/docs/handbook/declaration-files/introduction.html
- https://www.jb51.net/article/138217.htm
声明文件的两种捆绑方式
根据官方文档介绍,主要有两种将声明文件与 npm 包捆绑的方式:
- 方式一:与 npm 包直接捆绑在一起
- 方式二:发布到 npm 上的 @types 组织
第一种方式是指,开发者在 TypeScript 项目中通过 npm install 安装库,并在代码中 import 后,即可直接使用,无需额外配置。
当部分库官方不再维护时,可采用第二种方式。在 npm install 主库后,再执行 npm install @types/库名 来安装对应的声明文件。其原理是,TypeScript 2.0 版本之后,当 import 一个库且未在配置的 include 路径中找到时,会自动在 node_modules 的 @types 目录下查找该库的类型定义。
官方通常推荐第一种方式编写并发布声明文件。下面将通过具体示例展示第一种捆绑方式的实现过程。
本地捆绑声明文件示例
示例演示
首先初始化 TypeScript 项目,目录结构如下:

[原图,由程序原样保留,不描述看不到的内容]
{
"compilerOptions": {
"target": "es5",
/* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs",
/* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"allowJs": true,
"outDir": "./dist",
/* Redirect output structure to the directory. */
/* Allow javascript files to be compiled. */
"strict": true /* Enable all strict type-checking options. */
},
"include": [
"src/**/*"
]
}
tsconfig.json 配置如下:
const NAME = 'A';
let call = (who) => {
console.log('Hello ' + who + '! I am ' + NAME);
}
export default {
call
}
[原代码,由程序原样保留,不描述看不到的内容]
declare namespace a {
function call(who: string): void;
}
export default a;
可以看到,这里定义了一个模块 a,并为其捆绑了声明文件。模块 a 的实现代码 src/a/index.js 内容如下:
import a from './a';
a.call('Pwcong');
[原代码,由程序原样保留,不描述看不到的内容]

其对应的声明文件 src/a/index.d.ts 内容如下:
[原代码,由程序原样保留,不描述看不到的内容]

此时,我们可以在入口文件 src/index.ts 中引入模块 a:
{
"name": "b",
"version": "1.0.0",
"main": "./src/index.js",
"types": "./types/index.d.ts"
}
[原代码,由程序原样保留,不描述看不到的内容]
const NAME = 'B';
let call = (who) => {
console.log('Hello ' + who + '! I am ' + NAME);
}
module.exports = {
call
}
在命令行执行 tsc 编译后,会在 dist 目录中生成对应的 JavaScript 代码:
declare namespace b {
function call(who: string): void;
}
export = b;
[原图,由程序原样保留,不描述看不到的内容]
import a from './a';
a.call('Pwcong');
import b = require('b');
b.call('Pwcong');
执行命令 node ./dist/index.js 即可得到正确的输出结果。
模拟发布库的导入测试
接下来模拟导入发布后的 npm 库。在 node_modules 目录下创建目录 b,并初始化 Node 项目,此时的目录结构如下:
[原图,由程序原样保留,不描述看不到的内容]