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

TypeScript 入门教程

编写声明文件:全局库

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

TypeScriptTypeScript 入门教程声明文件.d.ts全局库declare namespace类型定义

本节目标:学会为全局库编写完整的 .d.ts 声明文件。你会掌握命名空间的组织方式、如何用 interface/type 声明可复用类型、全局函数的多种写法,以及让项目识别全局声明的配置方法。

全局库长什么样

全局库(Global Library)是最早的 JS 库形态。它不依赖任何模块加载器,直接在全局作用域挂载一个对象,通过 <script> 标签引入就能用。

典型特征是:

  • 在 HTML 里通过 <script src="..."> 引入
  • 代码里直接通过全局变量名访问,比如 $(jQuery)、_(lodash 全局版)
  • 源码里通常有对 windowglobalThis 的赋值

举个例子,一个叫 MyLib 的全局库,JS 侧可能是这样写的:

// mylib.js —— 全局库的实现
window.MyLib = (function() {
  var version = "2.0.0";

  function createMessage(name) {
    return "Hello, " + name + "!";
  }

  function showDialog(options) {
    console.log("Dialog:", options.title);
  }

  return {
    version: version,
    createMessage: createMessage,
    showDialog: showDialog
  };
})();

使用方式极其简单——不用 require,不用 import

<script src="mylib.js"></script>
<script>
  MyLib.showDialog({ title: "Welcome" });
</script>

第一步:分析 API 结构

在动手写声明文件之前,先梳理清楚库暴露了哪些东西:

  • 一个全局对象 MyLib
  • MyLib.version:字符串,只读
  • MyLib.createMessage(name):接收字符串,返回字符串
  • MyLib.showDialog(options):接收一个对象,有 title 和可选的 widthheight

分析清楚了再写声明,事半功倍。很多人在这一步偷懒,结果声明文件写出来对不上实际 API,反而引入了更多 bug。

基本声明写法

把上面的 API 翻译成 TypeScript 声明:

// mylib.d.ts
declare namespace MyLib {
  const version: string;

  function createMessage(name: string): string;

  function showDialog(options: {
    title: string;
    width?: number;
    height?: number;
  }): void;
}

这个文件放在项目里(比如 src/types/mylib.d.ts),然后在 tsconfig.jsoninclude 里包含它,项目中就能直接用 MyLib.xxx 且获得完整的类型提示。

把参数类型内联在函数签名里写(options: { title: string; ... })只适合简单场景。参数一复杂,你就会想把它抽成独立类型。

用 interface 组织可复用类型

showDialog 的参数可以抽出为一个 interface:

// mylib.d.ts(改进版)
declare namespace MyLib {
  const version: string;

  function createMessage(name: string): string;
  function showDialog(options: DialogOptions): void;

  interface DialogOptions {
    title: string;
    width?: number;
    height?: number;
  }
}

抽成 interface 有三个好处:

  1. 可复用:如果多个函数用同样的参数结构,interface 只用写一次。
  2. 方便使用方引用:别人可以 const opts: MyLib.DialogOptions = { ... } 把配置先定义好再传进去。
  3. 可扩展:用 interface 声明,以后还能通过声明合并(declaration merging)追加属性。
Tip

放在 namespace 内的 interface 仍然是类型(不会被编译为运行时代码),但它的访问路径是 MyLib.DialogOptions,和 JS 侧的 MyLib.showDialog 自然地在同一个命名空间下。

当全局库本身可调用

有些全局库暴露的是一个函数而不是对象。比如一个叫 initApp 的库,它本身就是一个函数,同时上面还挂了几个属性:

// initApp 在运行时是一个函数,上面挂了 version 属性
function initApp(config) {
  // ...
}
initApp.version = "3.0.0";
initApp.reset = function() { /* ... */ };

这种”可调用 + 有属性”的组合,在声明文件里要同时用 declare functiondeclare namespace

// initapp.d.ts
declare function initApp(config: InitConfig): void;

declare namespace initApp {
  const version: string;
  function reset(): void;

  interface InitConfig {
    apiKey: string;
    debug?: boolean;
  }
}

为什么 functionnamespace 同名可以共存?因为 TypeScript 允许同一个名字在不同上下文中承担不同角色:initApp 作为”值”是可调用函数,作为”命名空间”包含静态属性和类型。这叫声明合并,在 TS 的类型系统里是完全合法的。

函数重载的声明

全局函数经常需要支持多种不同参数组合。比如一个 log 函数,可以接收一个字符串或一个对象:

// logger.d.ts
declare function log(message: string): void;
declare function log(data: { level: string; message: string }): void;

重载声明的规则:

  • 每个 declare function 是一个独立的类型签名(重载)
  • 越具体的重载放在越前面(TS 按从上到下匹配第一个兼容的签名)
  • 参数类型的兼容性判断是”第一个能匹配的就被选中”

正确的顺序示例:

// ✅ 正确:具体在前,通用在后
declare function log(data: { level: string; message: string }): void;
declare function log(message: string): void;

如果反过来把通用签名 log(message: string) 放前面,对象参数就会先匹配到 string 版本(TS 的重载解析只看兼容,不挑”最佳”),导致类型推断错误。

完整的全局库声明模板

把上面的知识组合起来,就是一个完整、可扩展的全局库声明文件:

// mylib.d.ts —— 全局库声明模板
declare namespace MyLib {
  // 常量
  const version: string;

  // 函数
  function createMessage(name: string): string;
  function showDialog(options: DialogOptions): void;

  // 类型
  interface DialogOptions {
    title: string;
    width?: number;
    height?: number;
  }

  type LogLevel = "debug" | "info" | "warn" | "error";

  // 类(如果库有构造函数需要 new)
  class Logger {
    constructor(level: LogLevel);
    log(message: string): void;
  }

  // 枚举
  enum Status {
    Idle,
    Loading,
    Success,
    Error
  }
}

几点说明:

  • 所有类型(interface、type)放在 namespace 内部,避免污染全局。
  • 命名结构反映了 JS 运行时的真实 API 结构——运行时怎么用,声明文件就怎么写。
  • 类声明不需要 declare 前缀,因为在 namespace 内部不是顶层。

让项目识别全局声明

写好声明文件后,你需要让 TS 编译器加载它。

方式一:放在 include

{
  "compilerOptions": {
    "strict": true,
    "types": []
  },
  "include": ["src/**/*", "src/types/**/*.d.ts"]
}

把所有自定义的 .d.ts 统一放在 src/types/ 下,然后 include 整个目录。这是最直白的方式,适合声明文件跟着项目走的情况。

方式二:使用三斜线指令(不推荐新项目)

/// <reference path="./types/mylib.d.ts" />

这是早期 TypeScript 的做法,现在基本被 include/files 取代。只在一些遗留项目里能看到。

方式三:配置 typeRoots

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./src/types"]
  }
}

不过 TS 7.0 中 types: [] 是默认值(不再自动加载所有 @types),所以 typeRoots 的使用场景变少了。推荐用 include

一个常见坑:命名冲突

在全局声明文件中直接写顶层 interfacetype(不放在 namespace 里)容易跟别的库冲突:

// ❌ 不好:顶层声明,容易和别的库冲突
interface DialogOptions {
  title: string;
}
declare namespace MyLib {
  function showDialog(options: DialogOptions): void;
}

如果一个叫 OtherLib 的声明文件也定义了叫 DialogOptions 的接口,两者就会冲突。正确的做法是把类型包在 namespace 里:

// ✅ 好:类型在 namespace 内,不会冲突
declare namespace MyLib {
  interface DialogOptions {
    title: string;
  }
  function showDialog(options: DialogOptions): void;
}

遵守一个简单原则:全局库声明文件的命名空间就是库暴露的全局变量名。在这个命名空间内部,类型名怎么取都行,不会干扰外部。

总结

写全局库声明文件的思路:

  1. 先分析 JS 库的 API 结构——它暴露了什么变量、函数、类。
  2. declare namespace 包裹所有声明,避免命名污染。
  3. 函数用 declare function,支持重载(具体在前)。
  4. 参数复杂时用 interface 抽出来,放在 namespace 内。
  5. 通过 tsconfig.jsoninclude 让 TS 加载声明文件。

全局库声明是声明文件家族里最基础的一种模式。下一章我们转向模块库——那些需要 import 才能用的库,声明文件的写法完全不同。