摘要 :在实际项目中,我们不可能从零开始编写所有代码,经常需要使用第三方库(如 Lodash、Express、React)。这些库是用 JavaScript 编写的,如何让 TypeScript 理解它们的类型?如何为自定义模块编写类型声明?本文将系统讲解 TypeScript 的类型声明文件(.d.ts)、模块系统(ES Module 与 CommonJS 的兼容)、@types 的使用、全局类型声明、模块增强以及项目中的类型组织策略。
一、前言
经过前五篇文章,我们已经掌握了 TypeScript 的核心语法:基础类型、函数、接口、类、泛型以及高级类型。现在你可以编写带有完整类型信息的 TypeScript 代码了。
然而,实际开发中你会遇到两类情况:
使用别人写的 JavaScript 库:比如 Lodash、Axios,它们没有类型信息,如何在 TS 中安全调用?
编写自己的库或模块:如何让你写的模块也能被其他 TS 项目消费时享受类型提示?
答案就是类型声明文件 (Type Declaration Files,后缀 .d.ts)。
二、什么是类型声明文件(.d.ts)
2.1 作用与原理
类型声明文件只包含类型信息,没有具体实现(没有函数体、变量赋值等)。它的作用是告诉 TypeScript 编译器某个 JavaScript 模块或全局变量的形状(参数类型、返回值、属性等)。
例如,一个全局变量 myGlobal 是 JavaScript 运行时提供的,我们可以在 .d.ts 中声明:
TypeScript
// globals.d.ts
declare var myGlobal: string;
之后在 .ts 文件中就可以直接使用 myGlobal,不会报错。
原理 :TypeScript 编译器会读取 .d.ts 文件,将其作为类型上下文,但不会把它们编译到最终的 JavaScript 输出中(它们仅用于类型检查)。
2.2 声明文件的结构
一个典型的声明文件可能包含以下内容:
TypeScript
// types.d.ts
// 声明全局变量
declare const API_URL: string;
// 声明全局函数
declare function log(message: string): void;
// 声明全局类
declare class Person {
name: string;
constructor(name: string);
greet(): void;
}
// 声明模块(例如 node_modules 中的某个库)
declare module "my-library" {
export function doSomething(): void;
export const version: string;
}
// 声明 namespace(旧式)
declare namespace MyNamespace {
function foo(): void;
}
三、使用第三方库的类型
3.1 @types 命名空间
绝大多数流行的 JavaScript 库,社区已经提供了对应的类型声明包,存放在 DefinitelyTyped 仓库中,通过 npm 以 @types/ 前缀安装。
例如,为 Lodash 安装类型声明:
TypeScript
pnpm add -D @types/lodash

之后在 TS 文件中,直接 import * as _ from 'lodash',就能获得完整的类型提示。
常见库的安装命令:
| 库 | 命令 |
|---|---|
| lodash | pnpm add -D @types/lodash |
| express | pnpm add -D @types/express |
| react | pnpm add -D @types/react |
| node | pnpm add -D @types/node |
3.2 自带类型的库
有些库(如 React、Vue、Angular、TypeScript 自身)已经在 npm 包中包含了类型声明文件(通常位于 package.json 的 "types" 字段指向 .d.ts 文件)。这种情况下你不需要额外安装 @types/。
以 axios 为例,它是用 TypeScript 编写的,自带类型,所以直接安装即可享受类型提示。
3.3 找不到类型怎么办?
如果某个库既没有内置类型,也没有 @types/ 包,你有三个选择:
自己写一个简单的声明文件。
使用 any 逃生 :import * as lib from 'lib'; (lib as any).someMethod()。
贡献到 DefinitelyTyped(开源社区)。
四、为 JavaScript 模块编写声明文件
假设你有一个简单的 JavaScript 文件 utils.js:
TypeScript
// utils.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
你希望在 TypeScript 中安全地使用它。你需要创建一个同名的 .d.ts 文件(或集中管理)。
4.1 声明模块的方式
方式一:同名 .d.ts
在 utils.d.ts 中:
TypeScript
export function add(a: number, b: number): number;
export const PI: number;
之后在 TS 文件中 import { add, PI } from './utils.js',就能获得类型提示。
方式二:模块声明(适用于没有文件系统映射的模块)
如果是一个 Node 模块(安装在 node_modules 中),你可以在项目的 .d.ts 文件中使用 declare module:
TypeScript
// declarations.d.ts
declare module "awesome-lib" {
export function magic(input: string): number;
export const version: string;
}
4.2 声明全局变量(非模块化代码)
如果项目中有通过 <script> 标签引入的全局库(如 jQuery),可以用:
TypeScript
// jquery.d.ts
declare var $: any; // 或者更精确的类型
或者更精确:
TypeScript
interface JQuery {
(selector: string): HTMLElement;
// ...
}
declare var $: JQuery;
4.3 支持 CommonJS 导出
对于 module.exports = function() {} 这种导出方式,可以使用 export = 语法:
TypeScript
// some-lib.d.ts
declare function myLib(): void;
declare namespace myLib {
export const name: string;
}
export = myLib;
使用时:
TypeScript
import myLib = require('some-lib');
myLib();
myLib.name;
五、TypeScript 中的模块系统
TypeScript 兼容 ES Module 和 CommonJS 两种模块语法,并通过 tsconfig.json 中的 module 选项控制输出的模块格式。
5.1 ES Module(import / export)
这是 TypeScript 推荐的模块语法,与将来 JavaScript 标准一致。
导出:
TypeScript
// math.ts
export const pi = 3.14;
export function square(x: number) { return x * x; }
// 默认导出
export default class Calculator { /* ... */ }
导入:
TypeScript
import Calculator, { pi, square } from './math';
5.2 CommonJS(require / module.exports)
在 Node.js 环境中常见。TypeScript 同样支持,但需要开启 esModuleInterop 才能无缝混用。
导出:
TypeScript
// logger.ts
function log(message: string) { console.log(message); }
export = log;
导入:
TypeScript
import log = require('./logger');
log('hello');
5.3 混用与 esModuleInterop
当同时使用 ES Module 和 CommonJS 时,最好在 tsconfig.json 中设置:
TypeScript
{
"compilerOptions": {
"module": "commonjs", // 或 "esnext"、"node16" 等
"esModuleInterop": true,
"allowSyntheticDefaultImports": true
}
}
esModuleInterop 会生成辅助代码,让你能像导入 ES 模块一样导入 CommonJS 模块:import * as express from 'express' 也可以写成 import express from 'express'。
六、类型引用与三斜线指令(/// <reference />)
在早期 TypeScript 项目中,使用三斜线指令来显式引用其他声明文件。现代项目中已基本被 import 和 tsconfig.json 的 types 选项取代,但了解它有助于阅读旧代码。
TypeScript
/// <reference path="path/to/types.d.ts" />
这告诉编译器在编译时包含 types.d.ts 文件。现在推荐使用 tsconfig.json 的 include 和 exclude 字段。
不过三斜线指令在某些场景仍有用途:例如引用一个全局库的类型声明,且不想通过 import 引入任何实际代码时。
七、模块解析策略
TypeScript 需要知道 import 语句中的模块路径如何映射到实际文件。tsconfig.json 中的 moduleResolution 控制这一行为。
7.1 Classic 策略(已过时)
旧版 TypeScript 的默认策略,不常用。
7.2 Node 策略
模仿 Node.js 的模块解析规则:
-
相对路径:
./foo→./foo.ts、./foo.tsx、./foo.d.ts、./foo/index.ts等。 -
非相对路径:在
node_modules中逐层查找,并根据package.json的types或typings字段定位。
现代项目推荐使用 "moduleResolution": "node"(如果输出 CommonJS)或 "moduleResolution": "node16" / "bundler"(取决于你的运行环境)。
7.3 路径映射(paths)
可以在 tsconfig.json 中设置 paths 来简化导入路径:
TypeScript
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
之后可以 import utils from '@/utils'。
八、命名空间(namespace)
在 TypeScript 早期,模块系统尚不成熟时,namespace 用于组织代码。现在推荐使用 ES 模块,但你可能在旧项目中看到。
TypeScript
namespace MyNamespace {
export const version = '1.0';
export function doWork() {}
}
// 使用
MyNamespace.doWork();
namespace 编译后会生成 IIFE 包裹的对象。它与 ES 模块不冲突,但新项目不建议使用。
九、全局类型扩展与模块增强
有时你需要为已有的对象或模块添加额外的属性/方法。
9.1 为全局对象添加属性
例如,想在 window 上挂载 myApp 变量:
TypeScript
// global.d.ts
interface Window {
myApp: {
version: string;
};
}
之后在任意 .ts 文件中:
TypeScript
window.myApp = { version: '1.0' }; // 类型安全
9.2 为第三方模块扩展类型(模块增强)
假设你想为 express 的 Request 对象添加 user 属性:
TypeScript
// express-augmentation.d.ts
import 'express'; // 必须导入才能增强
declare module 'express' {
interface Request {
user?: {
id: number;
name: string;
};
}
}
然后在你的代码中,req.user 就有了类型定义。
注意 :模块增强需要放在 .d.ts 文件或全局模块中,并且确保被编译器包含。
十、项目中的类型组织最佳实践
将类型声明放在 src/types/ 目录下 ,使用 .d.ts 后缀。
为项目内共享的类型创建 types/common.ts ,使用 export interface 而不是 .d.ts 全局声明。
使用 tsconfig.json 的 include 字段明确包含类型文件:
TypeScript
{
"include": ["src/**/*", "types/**/*.d.ts"]
}
避免全局类型污染 :尽量使用模块化导出/导入,而不是 declare var。
对于第三方库缺少类型的情况 ,优先 @types/,实在没有则局部声明并考虑向上游贡献。
开启 strict 模式 (包含 strictNullChecks、noImplicitAny 等),强制类型安全。
十一、总结
本文全面讲解了 TypeScript 与外部世界交互的类型层面:
-
类型声明文件(
.d.ts) :为 JS 代码补充类型,理解declare语法。 -
使用第三方库的类型 :
@types/安装或自带类型,以及缺失时的应对方案。 -
编写自定义声明:为全局变量、模块、CommonJS 导出等提供类型。
-
模块系统 :ES Module 与 CommonJS 的兼容,
esModuleInterop的作用。 -
模块解析 :
moduleResolution选项与路径映射。 -
命名空间(了解即可):旧式组织代码方式。
-
全局扩展与模块增强:为已有类型添加新成员。
-
项目组织最佳实践:保持类型清晰、可控。
至此,你已经掌握了 TypeScript 在日常开发中所需的所有核心知识点。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。