编写声明文件:模块库
本教程共 80 篇 · 第 63 篇 · 更新于 2026-08-10 · 约 15 分钟阅读
本节目标:掌握为模块库(ES Module / CommonJS)编写 .d.ts 声明文件的全部模式。你会学到 export 声明、默认导出、
declare module的用法,以及 TS 7.0 移除 UMD 后声明文件的写法变化。
模块库 vs 全局库
上一章我们写了全局库的声明——那种不用 import,直接在全局作用域访问的库。但在现代 JS 生态里,绝大多数库都是模块库:你需要通过 import 或 require 来加载。
// 全局库:直接用,不需要导入
MyLib.showDialog({ title: "Hello" });
// 模块库:必须 import
import { showDialog } from "mylib";
showDialog({ title: "Hello" });
声明文件的写法也因此完全不同——全局库用 declare namespace,模块库用 export。
基础模块声明
假设有一个 npm 包 string-utils,它的 index.js 长这样:
// string-utils/index.js
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function truncate(str, maxLength) {
return str.length > maxLength ? str.slice(0, maxLength) + "..." : str;
}
module.exports = { capitalize, truncate };
它的声明文件只需要一层层 export 每个函数即可:
// string-utils.d.ts
export function capitalize(str: string): string;
export function truncate(str: string, maxLength: number): string;
注意这个文件顶层有 export,所以 TS 把它当成模块声明文件。里面的声明不会泄漏到全局作用域,只有显式 import 才能用。
使用方:
import { capitalize } from "string-utils";
capitalize("hello"); // 类型安全
导出类型
模块不仅可以导出函数和变量,也可以导出类型(interface、type):
// string-utils.d.ts
export interface TruncateOptions {
maxLength: number;
ellipsis?: string;
preserveWords?: boolean;
}
export function capitalize(str: string): string;
export function truncate(str: string, options: TruncateOptions): string;
使用方可以同时导入值和类型:
import { truncate, TruncateOptions } from "string-utils";
const opts: TruncateOptions = {
maxLength: 50,
ellipsis: "……"
};
truncate("这是一段很长的文本", opts);
在 TS 中,import 一个类型和 import 一个值语法完全一样。TS 知道 TruncateOptions 是纯类型,编译时会直接擦除,不会留下任何 JS 代码。
export default 声明
如果模块使用 export default 暴露一个主入口,声明文件用 export default:
// calculator.d.ts
export default class Calculator {
constructor(initial?: number);
value: number;
add(n: number): this;
subtract(n: number): this;
multiply(n: number): this;
result(): number;
}
导入方式:
import Calculator from "calculator";
const calc = new Calculator(10);
calc.add(5).multiply(2);
当模块导出的是函数
有些模块的主导出是一个函数。比如 express 的 require("express") 返回的是一个函数:
// express-like.d.ts
declare function createApp(): App;
declare namespace createApp {
export interface App {
listen(port: number, callback?: () => void): void;
get(path: string, handler: (req: Request, res: Response) => void): void;
}
export interface Request {
url: string;
method: string;
}
export interface Response {
send(body: string): void;
status(code: number): Response;
}
}
export = createApp;
这里用了 export =(CommonJS 兼容语法)。它告诉 TypeScript:这个模块的 module.exports 就是 createApp 函数本身,同时 createApp 的 namespace 里还挂着 App、Request、Response 等类型。
Note
export =是 TypeScript 兼容 CommonJS 模块系统的语法,对于 ES Module 项目,更推荐用标准的export default或命名导出。TS 7.0 移除了moduleResolution: node10,意味着export =的使用场景会越来越少,但你仍然可能在老旧声明文件中看到它。
declare module:为没有声明文件的模块补类型
有时你引入了一个 npm 包,它既没有自带的 .d.ts,@types/xxx 也不存在。这时可以用 declare module 给它临时补上类型。
// custom-types.d.ts
declare module "some-untyped-lib" {
export function doStuff(input: string): number;
export function doOtherStuff(input: number): string;
export interface StuffOptions {
timeout: number;
retries?: number;
}
}
这个声明文件告诉 TS:“some-untyped-lib 这个模块存在,它导出了 doStuff、doOtherStuff 函数和一个 StuffOptions 类型。”
文件可以放在项目的任意位置,只要被 tsconfig.json 的 include 扫到就行。放 src/types/ 目录是个常见约定。
声明文件的路径匹配
declare module 里的字符串必须精确匹配导入路径。如果你 import 写的是:
import { something } from "@scope/package/submodule";
那 declare module 也必须用完全相同的字符串:
declare module "@scope/package/submodule" {
export function something(): void;
}
通配符也支持,用 *:
// 匹配所有以 .css 结尾的导入
declare module "*.css" {
const content: Record<string, string>;
export default content;
}
// 匹配所有 .png 文件
declare module "*.png" {
const src: string;
export default src;
}
这个技巧在 Vite 或 Webpack 项目中特别常用——给非 JS/TS 资源文件补上类型,消除 TS 对 import "./style.css" 的报错。
命名空间导出
如果一个模块的 API 很多,希望用户按命名空间使用(比如 import * as utils),声明文件用嵌套 namespace 组织:
// utils.d.ts
export namespace StringUtils {
export function capitalize(str: string): string;
export function truncate(str: string, max: number): string;
}
export namespace NumberUtils {
export function clamp(value: number, min: number, max: number): number;
export function random(min: number, max: number): number;
}
用法:
import { StringUtils, NumberUtils } from "utils";
StringUtils.capitalize("hello");
NumberUtils.clamp(42, 0, 100);
TS 7.0 的模块变化
TS 7.0 做了一些关于模块的重大变更,写声明文件时需要留意:
移除了 AMD / UMD / SystemJS 模块格式。如果你的声明文件里还残留着 export as namespace xxx(这是 UMD 声明时代的产物),在 7.0 项目中这个声明虽然不会报错,但它对应的模块格式已经不存在了。
默认 module: "esnext"。新的 tsconfig.json 默认目标就是 ES 模块。你用 import/export 写声明文件,跟 TS 7.0 的默认配置完美对应。
移除了 moduleResolution: node10。这对声明文件的影响是:以前靠 node10 自动解析的一些路径约定(比如省略扩展名、自动找 index.d.ts)现在行为可能不同。推荐使用 node16 或 bundler 解析策略,更现代且和 Node.js 原生行为一致。
types: [] 是新的默认值。你不会再因为安装了 @types/xxx 而自动加载它们的声明文件——需要显式在 types 数组中声明,或者通过 /// <reference types="..." /> 手动引用。
从 CommonJS 到 ES Module 的声明迁移
很多旧的声明文件用 export = 配合 import ... = require(...) 的模式:
// 旧写法(CommonJS 风格)
// mylib.d.ts
export = MyLib;
declare function MyLib(): void;
declare namespace MyLib {
// ...
}
// 旧写法使用方式
import MyLib = require("mylib");
如果库本身已经升级为 ES Module,声明文件也应该改成现代写法:
// 新写法(ES Module 风格)
// mylib.d.ts
export default function MyLib(): void;
export declare namespace MyLib {
// ...
}
// 新写法使用方式
import MyLib from "mylib";
TS 7.0 默认 module: "esnext",你写的声明文件应该尽量用 export/export default 而不是 export =。
总结
模块库声明文件的核心原则:
- 顶层有
import/export就是模块声明,声明不会泄漏到全局。 export导出函数、类、变量、类型(interface/type)——语法和普通 TS 一样。export default处理默认导出。- 函数型模块用
declare function+declare namespace组合,再export =。 declare module "xxx"给无类型包临时补类型,路径字符串必须精确匹配。- TS 7.0 移除了 UMD/AMD,声明文件尽量用 ES Module 写法。
下一章处理一个常见场景:库本身类型已经有了,但你用了一个插件/扩展库,类型怎么加?这就是模块扩充和插件声明。