首页 / TypeScript 入门教程 / 编写声明文件:模块库

TypeScript 入门教程

编写声明文件:模块库

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

TypeScriptTypeScript 入门教程声明文件.d.ts模块库exportdeclare module

本节目标:掌握为模块库(ES Module / CommonJS)编写 .d.ts 声明文件的全部模式。你会学到 export 声明、默认导出、declare module 的用法,以及 TS 7.0 移除 UMD 后声明文件的写法变化。

模块库 vs 全局库

上一章我们写了全局库的声明——那种不用 import,直接在全局作用域访问的库。但在现代 JS 生态里,绝大多数库都是模块库:你需要通过 importrequire 来加载。

// 全局库:直接用,不需要导入
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);

当模块导出的是函数

有些模块的主导出是一个函数。比如 expressrequire("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 里还挂着 AppRequestResponse 等类型。

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 这个模块存在,它导出了 doStuffdoOtherStuff 函数和一个 StuffOptions 类型。”

文件可以放在项目的任意位置,只要被 tsconfig.jsoninclude 扫到就行。放 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)现在行为可能不同。推荐使用 node16bundler 解析策略,更现代且和 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 写法。

下一章处理一个常见场景:库本身类型已经有了,但你用了一个插件/扩展库,类型怎么加?这就是模块扩充和插件声明。