本文献给:

已掌握 TypeScript 基础类型、泛型、高级类型等知识的开发者。本文将系统讲解 TypeScript 中模块与声明文件的相关知识,包括 .d.ts 声明文件的作用与编写、使用 @types 安装第三方库类型、为自己的 JavaScript 库生成类型声明,以及模块扩充与全局扩充的高级技巧,帮助你更好地与第三方库协作并发布自己的类型化库。


你将学到:

  1. 类型声明文件(.d.ts)的作用与编写
  2. declare 关键字的使用场景
  3. 使用 @types 安装 DefinitelyTyped 提供的类型定义
  4. 类型查找规则(typeRootstypes
  5. 为自己的 JS 库生成 .d.ts 并发布
  6. 模块扩充(Module Augmentation)与全局扩充
  7. 扩展内置原型(如 Array)的类型



一、类型声明文件(.d.ts)的作用与编写

1.1 什么是声明文件

TypeScript 的类型声明文件以 .d.ts 为后缀,只包含类型信息,不包含具体实现。它们用于为 JavaScript 代码(包括第三方库或遗留代码)提供类型描述,让 TypeScript 能够理解 JavaScript 代码的类型结构。

声明文件不会影响运行时的 JavaScript 输出,只用于编译时的类型检查。

1.2 declare 关键字

declare 关键字用于告诉 TypeScript:“某个变量、函数或类在别处已经定义,请按此类型理解。”通常用于描述全局变量、函数、类或模块。

声明全局变量

// globals.d.ts
declare const APP_VERSION: string;
declare const API_BASE_URL: string;

在代码中就可以直接使用:

console.log(APP_VERSION); // 类型为 string

声明全局函数

// globals.d.ts
declare function log(message: string): void;

声明全局类

declare class Person {
    constructor(name: string);
    name: string;
    greet(): void;
}

声明全局对象(命名空间)

declare namespace MyLibrary {
    function doSomething(): void;
    const version: string;
}

1.3 为 CDN 引入的库编写声明

如果一个库通过 CDN(如 <script> 标签)引入,没有 npm 包也没有类型文件,可以手动编写声明。

假设库 oldLib 挂载全局对象 OldLib,其 API 如下:

// 运行时存在
window.OldLib = {
    init(selector) { ... },
    version: "2.0"
};

编写声明文件 oldLib.d.ts

declare namespace OldLib {
    function init(selector: string): void;
    const version: string;
}

然后在 tsconfig.json 中确保包含了该声明文件。

1.4 声明模块

对于没有类型声明的 npm 包,可以快速编写模块声明。

// 声明一个模块,所有导入都是 any 类型
declare module "some-untyped-lib";

更精确的声明:

declare module "my-custom-module" {
    export function doSomething(input: string): number;
    export const version: string;
}

二、使用 @types 安装第三方库的类型

2.1 DefinitelyTyped 简介

DefinitelyTyped 是一个社区维护的 TypeScript 类型定义仓库,包含了数千个流行 JavaScript 库的类型声明。包名通常为 @types/库名

2.2 安装与使用

# 安装 lodash 的类型定义
npm install --save-dev @types/lodash

# 安装 axios 的类型定义
npm install --save-dev @types/axios

安装后,TypeScript 编译器会自动识别这些类型(通过 node_modules/@types 目录)。代码中直接导入即可获得类型提示。

import _ from "lodash";
import axios from "axios";

_.chunk([1,2,3], 2);       // 类型安全
axios.get("/api/users");   // 类型推断

2.3 类型查找规则

TypeScript 在解析模块的类型时按以下顺序查找:

  1. 检查 package.json 中的 "types""typings" 字段,指向一个声明文件。
  2. 检查 node_modules/@types/模块名 目录。
  3. 检查模块自身是否包含 .d.ts 文件(如 index.d.ts)。

可以通过 tsconfig.json 中的 typeRootstypes 配置自定义查找。

typeRoots:指定类型定义根目录数组。

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

types:限制只包含某些类型包(其他不会被自动引入)。

{
    "compilerOptions": {
        "types": ["node", "jest"]
    }
}

2.4 处理没有类型定义的库

如果库既没有内置类型,也没有 @types 包,可以:

  • 在项目内创建类型声明文件(如 src/types/module-name.d.ts),使用 declare module 简单声明。
  • 设置 "noImplicitAny": false 临时容忍(不推荐)。
  • 给库贡献类型到 DefinitelyTyped。

三、为自己的 JS 库生成 .d.ts

3.1 自动生成声明文件

如果使用 TypeScript 编写库,只需在 tsconfig.json 中开启 declaration 选项,编译时就会自动生成 .d.ts 文件。

{
    "compilerOptions": {
        "declaration": true,
        "declarationDir": "./types",      // 指定输出目录(可选)
        "outDir": "./dist"
    }
}

编译后,每个 .ts 文件会生成对应的 .d.ts.js 文件。

也可以生成单个声明文件:

{
    "compilerOptions": {
        "declaration": true,
        "declarationMap": true,     // 生成声明文件的 sourcemap
        "emitDeclarationOnly": true // 只生成声明文件,不生成 JS(用于特殊场景)
    }
}

3.2 为 JavaScript 库手动编写声明

如果有一个纯 JavaScript 库(或需要为已有 JS 代码补类型),可以手动编写 .d.ts 文件。

示例:mathlib.js

export function add(a, b) { return a + b; }
export const PI = 3.14159;

对应的 mathlib.d.ts

export function add(a: number, b: number): number;
export const PI: number;

支持默认导出

declare function greet(name: string): string;
export default greet;

重载

export function format(value: string): string;
export function format(value: number): string;

3.3 发布支持 TypeScript 的 npm 包

发布一个既支持 JavaScript 又提供类型定义的包,需要在 package.json 中配置:

{
    "name": "my-awesome-lib",
    "main": "dist/index.js",
    "types": "dist/index.d.ts",
    "scripts": {
        "build": "tsc"
    },
    "files": ["dist"]
}
  • "main":指向 CommonJS 入口。
  • "module"(可选):指向 ES 模块入口。
  • "types""typings":指向声明文件。

如果发布的是纯 JS 项目,但提供了手写的 .d.ts,同样配置 "types" 字段。

3.4 检查类型声明质量

推荐使用 @arethetypeswrong/clitsc --noEmit 验证类型定义与实现的一致性。

npx @arethetypeswrong/cli --pack .

四、模块扩充(Module Augmentation)与全局扩充

模块扩充允许在声明文件或模块中为已存在的模块添加额外的属性或方法。这对扩展第三方库或内置对象非常有用。

4.1 为第三方模块添加额外属性

假设 expressRequest 对象缺少一个 user 属性,我们可以扩充其类型。

// types/express-augmentation.d.ts
import { Request } from "express";

declare module "express" {
    interface Request {
        user?: {
            id: number;
            name: string;
        };
    }
}

之后在任何使用 express 的地方,req.user 都会拥有正确的类型。

4.2 全局命名空间扩充

在模块文件中(有 import/export),可以通过 declare global 扩充全局作用域。

// global-augment.ts
export {};

declare global {
    interface Window {
        myCustomProperty: string;
    }
    
    var myGlobalVar: number;
}

之后就可以使用:

window.myCustomProperty = "hello";
myGlobalVar = 42;

4.3 扩展 Array 原型方法

虽然不推荐修改内置原型,但在某些场景下需要,并添加类型支持。

第一步:在运行时添加方法

// array-polyfill.ts
if (!Array.prototype.first) {
    Array.prototype.first = function<T>(): T | undefined {
        return this.length > 0 ? this[0] : undefined;
    };
}

第二步:扩充类型声明

// array-augmentation.d.ts
interface Array<T> {
    first(): T | undefined;
}

确保扩充文件被 TypeScript 包含(可以放在 src/types/ 并配置 typeRoots 或通过 tsconfig.jsoninclude 包含)。

4.4 模块扩充的最佳实践

  • 将模块扩充放在独立的 .d.ts.ts 文件中(确保该文件是模块,即至少有一个 import/export,或者显式写 export {})。
  • 避免重复扩充同一模块的不同文件导致冲突。
  • 如果扩充的是自己的模块,直接修改原声明文件更好。

五、常见错误与注意事项

5.1 声明文件未被 TypeScript 识别

  • 确认文件后缀是 .d.ts
  • 确认在 tsconfig.jsonincludefiles 中包含了该文件。
  • 检查 types 配置是否限制了自动引入。

5.2 全局声明冲突

多个声明文件声明了同名的全局变量可能会导致冲突。尽量使用模块化导入,避免全局污染。

5.3 模块扩充不生效

  • 确保扩充的语法正确:declare module "module-name" { ... }
  • 扩充文件必须作为一个模块被加载(即至少有一条 importexport)。
  • 确认 module-name 与实际导入的模块名完全一致(包括大小写和路径)。

5.4 发布包时忘记包含声明文件

检查 package.jsonfiles 字段是否包含 .d.ts 文件。运行 npm pack 查看打包内容。

5.5 使用 @types 与库内置类型重复

某些库(如 uuidaxios 部分版本)已经内置了类型声明,再安装 @types/xxx 可能导致重复或冲突。先检查 node_modules/库名/package.json 是否有 types 字段。

5.6 为 CDN 库编写声明时忽略全局命名空间

如果库以 UMD 格式同时支持 script 标签和模块导入,声明文件应包含导出定义和全局定义。

export as namespace MyLib;
export = MyLib;

declare namespace MyLib {
    function doIt(): void;
}

六、综合示例

// ---------- 1. 全局声明文件 (globals.d.ts) ----------
declare const ENV: "development" | "production";
declare function logError(message: string): void;

// ---------- 2. 为无类型的第三方库声明模块 (declarations/old-parser.d.ts) ----------
declare module "old-parser" {
    export function parse(input: string): object;
    export const version: string;
}

// ---------- 3. 使用这些声明 ----------
// app.ts
console.log(ENV); // 类型为 "development" | "production"
logError("Something went wrong");

import parser from "old-parser";
const result = parser.parse("{a:1}");
console.log(result);

// ---------- 4. 模块扩充:为 express 添加自定义属性 ----------
// types/express-augment.d.ts
import { Request } from "express";

declare module "express" {
    interface Request {
        startTime?: number;
    }
}

// ---------- 5. 全局扩充:为 window 添加属性 ----------
// global-augment.ts (必须是一个模块)
export {};

declare global {
    interface Window {
        __APP_CONFIG__: {
            apiUrl: string;
        };
    }
}

// 使用时
window.__APP_CONFIG__ = { apiUrl: "https://api.com" };

// ---------- 6. 为自己的 JS 库生成 .d.ts ----------
// 假设源文件 utils.js
// export function add(a, b) { return a + b; }
// 手动编写 utils.d.ts
/*
export function add(a: number, b: number): number;
*/

// ---------- 7. 扩展 Array 原型 ----------
// array-ext.ts
export {};

declare global {
    interface Array<T> {
        last(): T | undefined;
    }
}

if (!Array.prototype.last) {
    Array.prototype.last = function<T>(): T | undefined {
        return this.length > 0 ? this[this.length - 1] : undefined;
    };
}

// 使用
const arr = [1, 2, 3];
console.log(arr.last()); // 3

七、小结

概念 语法/示例 说明
声明文件 .d.ts 后缀 仅类型,无实现
declare declare const x: number 声明全局变量/函数/类
模块声明 declare module "name" { export ... } 描述无类型 npm 包
@types npm install @types/lodash 安装社区类型定义
类型查找规则 typeRootstypes 控制类型包查找
自动生成声明文件 tsc --declaration 从 TS 源码生成 .d.ts
发布包类型 package.json 中的 "types" 字段 指向声明文件入口
模块扩充 declare module "express" { interface Request { ... } } 为第三方模块添加类型
全局扩充 declare global { var x: number; } 在模块中扩充全局命名空间
内置原型扩展 interface Array<T> { newMethod(): ... } 为内置对象添加类型



觉得文章有帮助?别忘了:

👍 点赞 👍 – 给我一点鼓励
⭐ 收藏 ⭐ – 方便以后查看
🔔 关注 🔔 – 获取更新通知



标签: #TypeScript #声明文件 #d.ts #@types #模块扩充 #学习笔记 #前端开发

更多推荐