摘要在实际项目中我们不可能从零开始编写所有代码经常需要使用第三方库如 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.ts2.1 作用与原理类型声明文件只包含类型信息没有具体实现没有函数体、变量赋值等。它的作用是告诉 TypeScript 编译器某个 JavaScript 模块或全局变量的形状参数类型、返回值、属性等。例如一个全局变量myGlobal是 JavaScript 运行时提供的我们可以在.d.ts中声明// globals.d.ts declare var myGlobal: string;之后在.ts文件中就可以直接使用myGlobal不会报错。原理TypeScript 编译器会读取.d.ts文件将其作为类型上下文但不会把它们编译到最终的 JavaScript 输出中它们仅用于类型检查。2.2 声明文件的结构一个典型的声明文件可能包含以下内容// 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.1types命名空间绝大多数流行的 JavaScript 库社区已经提供了对应的类型声明包存放在DefinitelyTyped仓库中通过 npm 以types/前缀安装。例如为 Lodash 安装类型声明pnpm add -D types/lodash之后在 TS 文件中直接import * as _ from lodash就能获得完整的类型提示。常见库的安装命令库命令lodashpnpm add -D types/lodashexpresspnpm add -D types/expressreactpnpm add -D types/reactnodepnpm add -D types/node3.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// 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中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// declarations.d.ts declare module awesome-lib { export function magic(input: string): number; export const version: string; }4.2 声明全局变量非模块化代码如果项目中有通过script标签引入的全局库如 jQuery可以用// jquery.d.ts declare var $: any; // 或者更精确的类型或者更精确interface JQuery { (selector: string): HTMLElement; // ... } declare var $: JQuery;4.3 支持 CommonJS 导出对于module.exports function() {}这种导出方式可以使用export 语法// some-lib.d.ts declare function myLib(): void; declare namespace myLib { export const name: string; } export myLib;使用时import myLib require(some-lib); myLib(); myLib.name;五、TypeScript 中的模块系统TypeScript 兼容 ES Module 和 CommonJS 两种模块语法并通过tsconfig.json中的module选项控制输出的模块格式。5.1 ES Moduleimport/export这是 TypeScript 推荐的模块语法与将来 JavaScript 标准一致。导出// math.ts export const pi 3.14; export function square(x: number) { return x * x; } // 默认导出 export default class Calculator { /* ... */ }导入import Calculator, { pi, square } from ./math;5.2 CommonJSrequire/module.exports在 Node.js 环境中常见。TypeScript 同样支持但需要开启esModuleInterop才能无缝混用。导出// logger.ts function log(message: string) { console.log(message); } export log;导入import log require(./logger); log(hello);5.3 混用与esModuleInterop当同时使用 ES Module 和 CommonJS 时最好在tsconfig.json中设置{ 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选项取代但了解它有助于阅读旧代码。/// reference pathpath/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来简化导入路径{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }之后可以import utils from /utils。八、命名空间namespace在 TypeScript 早期模块系统尚不成熟时namespace用于组织代码。现在推荐使用 ES 模块但你可能在旧项目中看到。namespace MyNamespace { export const version 1.0; export function doWork() {} } // 使用 MyNamespace.doWork();namespace编译后会生成 IIFE 包裹的对象。它与 ES 模块不冲突但新项目不建议使用。九、全局类型扩展与模块增强有时你需要为已有的对象或模块添加额外的属性/方法。9.1 为全局对象添加属性例如想在window上挂载myApp变量// global.d.ts interface Window { myApp: { version: string; }; }之后在任意.ts文件中window.myApp { version: 1.0 }; // 类型安全9.2 为第三方模块扩展类型模块增强假设你想为express的Request对象添加user属性// 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字段明确包含类型文件{ include: [src/**/*, types/**/*.d.ts] }避免全局类型污染尽量使用模块化导出/导入而不是declare var。对于第三方库缺少类型的情况优先types/实在没有则局部声明并考虑向上游贡献。开启strict模式包含strictNullChecks、noImplicitAny等强制类型安全。十一、总结本文全面讲解了 TypeScript 与外部世界交互的类型层面类型声明文件.d.ts为 JS 代码补充类型理解declare语法。使用第三方库的类型types/安装或自带类型以及缺失时的应对方案。编写自定义声明为全局变量、模块、CommonJS 导出等提供类型。模块系统ES Module 与 CommonJS 的兼容esModuleInterop的作用。模块解析moduleResolution选项与路径映射。命名空间了解即可旧式组织代码方式。全局扩展与模块增强为已有类型添加新成员。项目组织最佳实践保持类型清晰、可控。至此你已经掌握了 TypeScript 在日常开发中所需的所有核心知识点。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。动手练习是掌握编程最快的方法请务必亲手敲一遍本文的所有示例代码并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源谢谢大家。
TypeScript 从零基础到精通(六):类型声明与模块化
摘要在实际项目中我们不可能从零开始编写所有代码经常需要使用第三方库如 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.ts2.1 作用与原理类型声明文件只包含类型信息没有具体实现没有函数体、变量赋值等。它的作用是告诉 TypeScript 编译器某个 JavaScript 模块或全局变量的形状参数类型、返回值、属性等。例如一个全局变量myGlobal是 JavaScript 运行时提供的我们可以在.d.ts中声明// globals.d.ts declare var myGlobal: string;之后在.ts文件中就可以直接使用myGlobal不会报错。原理TypeScript 编译器会读取.d.ts文件将其作为类型上下文但不会把它们编译到最终的 JavaScript 输出中它们仅用于类型检查。2.2 声明文件的结构一个典型的声明文件可能包含以下内容// 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.1types命名空间绝大多数流行的 JavaScript 库社区已经提供了对应的类型声明包存放在DefinitelyTyped仓库中通过 npm 以types/前缀安装。例如为 Lodash 安装类型声明pnpm add -D types/lodash之后在 TS 文件中直接import * as _ from lodash就能获得完整的类型提示。常见库的安装命令库命令lodashpnpm add -D types/lodashexpresspnpm add -D types/expressreactpnpm add -D types/reactnodepnpm add -D types/node3.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// 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中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// declarations.d.ts declare module awesome-lib { export function magic(input: string): number; export const version: string; }4.2 声明全局变量非模块化代码如果项目中有通过script标签引入的全局库如 jQuery可以用// jquery.d.ts declare var $: any; // 或者更精确的类型或者更精确interface JQuery { (selector: string): HTMLElement; // ... } declare var $: JQuery;4.3 支持 CommonJS 导出对于module.exports function() {}这种导出方式可以使用export 语法// some-lib.d.ts declare function myLib(): void; declare namespace myLib { export const name: string; } export myLib;使用时import myLib require(some-lib); myLib(); myLib.name;五、TypeScript 中的模块系统TypeScript 兼容 ES Module 和 CommonJS 两种模块语法并通过tsconfig.json中的module选项控制输出的模块格式。5.1 ES Moduleimport/export这是 TypeScript 推荐的模块语法与将来 JavaScript 标准一致。导出// math.ts export const pi 3.14; export function square(x: number) { return x * x; } // 默认导出 export default class Calculator { /* ... */ }导入import Calculator, { pi, square } from ./math;5.2 CommonJSrequire/module.exports在 Node.js 环境中常见。TypeScript 同样支持但需要开启esModuleInterop才能无缝混用。导出// logger.ts function log(message: string) { console.log(message); } export log;导入import log require(./logger); log(hello);5.3 混用与esModuleInterop当同时使用 ES Module 和 CommonJS 时最好在tsconfig.json中设置{ 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选项取代但了解它有助于阅读旧代码。/// reference pathpath/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来简化导入路径{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }之后可以import utils from /utils。八、命名空间namespace在 TypeScript 早期模块系统尚不成熟时namespace用于组织代码。现在推荐使用 ES 模块但你可能在旧项目中看到。namespace MyNamespace { export const version 1.0; export function doWork() {} } // 使用 MyNamespace.doWork();namespace编译后会生成 IIFE 包裹的对象。它与 ES 模块不冲突但新项目不建议使用。九、全局类型扩展与模块增强有时你需要为已有的对象或模块添加额外的属性/方法。9.1 为全局对象添加属性例如想在window上挂载myApp变量// global.d.ts interface Window { myApp: { version: string; }; }之后在任意.ts文件中window.myApp { version: 1.0 }; // 类型安全9.2 为第三方模块扩展类型模块增强假设你想为express的Request对象添加user属性// 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字段明确包含类型文件{ include: [src/**/*, types/**/*.d.ts] }避免全局类型污染尽量使用模块化导出/导入而不是declare var。对于第三方库缺少类型的情况优先types/实在没有则局部声明并考虑向上游贡献。开启strict模式包含strictNullChecks、noImplicitAny等强制类型安全。十一、总结本文全面讲解了 TypeScript 与外部世界交互的类型层面类型声明文件.d.ts为 JS 代码补充类型理解declare语法。使用第三方库的类型types/安装或自带类型以及缺失时的应对方案。编写自定义声明为全局变量、模块、CommonJS 导出等提供类型。模块系统ES Module 与 CommonJS 的兼容esModuleInterop的作用。模块解析moduleResolution选项与路径映射。命名空间了解即可旧式组织代码方式。全局扩展与模块增强为已有类型添加新成员。项目组织最佳实践保持类型清晰、可控。至此你已经掌握了 TypeScript 在日常开发中所需的所有核心知识点。如果这篇文章帮你解决了实操上的困惑别忘记点击点赞、分享也可以留言告诉我你遇到的其它问题我会尽快回复。动手练习是掌握编程最快的方法请务必亲手敲一遍本文的所有示例代码并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源谢谢大家。