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.ts

2.1 作用与原理

类型声明文件只包含类型信息,没有具体实现(没有函数体、变量赋值等)。它的作用是告诉 TypeScript 编译器某个 JavaScript 模块或全局变量的形状(参数类型、返回值、属性等)。

例如,一个全局变量 myGlobal 是 JavaScript 运行时提供的,我们可以在 .d.ts 中声明:

TypeScript 复制代码
// globals.d.ts
declare var myGlobal: string;

之后在 .ts 文件中就可以直接使用 myGlobal,不会报错。

原理 :TypeScript 编译器会读取 .d.ts 文件,将其作为类型上下文,但不会把它们编译到最终的 JavaScript 输出中(它们仅用于类型检查)。

2.2 声明文件的结构

一个典型的声明文件可能包含以下内容:

TypeScript 复制代码
// 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.1 @types 命名空间

绝大多数流行的 JavaScript 库,社区已经提供了对应的类型声明包,存放在 DefinitelyTyped 仓库中,通过 npm 以 @types/ 前缀安装。

例如,为 Lodash 安装类型声明:

TypeScript 复制代码
pnpm add -D @types/lodash

之后在 TS 文件中,直接 import * as _ from 'lodash',就能获得完整的类型提示。

常见库的安装命令

命令
lodash pnpm add -D @types/lodash
express pnpm add -D @types/express
react pnpm add -D @types/react
node pnpm add -D @types/node

3.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

TypeScript 复制代码
// 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 中:

TypeScript 复制代码
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

TypeScript 复制代码
// declarations.d.ts
declare module "awesome-lib" {
  export function magic(input: string): number;
  export const version: string;
}

4.2 声明全局变量(非模块化代码)

如果项目中有通过 <script> 标签引入的全局库(如 jQuery),可以用:

TypeScript 复制代码
// jquery.d.ts
declare var $: any;  // 或者更精确的类型

或者更精确:

TypeScript 复制代码
interface JQuery {
  (selector: string): HTMLElement;
  // ...
}
declare var $: JQuery;

4.3 支持 CommonJS 导出

对于 module.exports = function() {} 这种导出方式,可以使用 export = 语法:

TypeScript 复制代码
// some-lib.d.ts
declare function myLib(): void;
declare namespace myLib {
  export const name: string;
}
export = myLib;

使用时:

TypeScript 复制代码
import myLib = require('some-lib');
myLib();
myLib.name;

五、TypeScript 中的模块系统

TypeScript 兼容 ES Module 和 CommonJS 两种模块语法,并通过 tsconfig.json 中的 module 选项控制输出的模块格式。

5.1 ES Module(import / export

这是 TypeScript 推荐的模块语法,与将来 JavaScript 标准一致。

导出

TypeScript 复制代码
// math.ts
export const pi = 3.14;
export function square(x: number) { return x * x; }
​
// 默认导出
export default class Calculator { /* ... */ }

导入

TypeScript 复制代码
import Calculator, { pi, square } from './math';

5.2 CommonJS(require / module.exports

在 Node.js 环境中常见。TypeScript 同样支持,但需要开启 esModuleInterop 才能无缝混用。

导出

TypeScript 复制代码
// logger.ts
function log(message: string) { console.log(message); }
export = log;

导入

TypeScript 复制代码
import log = require('./logger');
log('hello');

5.3 混用与 esModuleInterop

当同时使用 ES Module 和 CommonJS 时,最好在 tsconfig.json 中设置:

TypeScript 复制代码
{
  "compilerOptions": {
    "module": "commonjs",      // 或 "esnext"、"node16" 等
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

esModuleInterop 会生成辅助代码,让你能像导入 ES 模块一样导入 CommonJS 模块:import * as express from 'express' 也可以写成 import express from 'express'


六、类型引用与三斜线指令(/// <reference />

在早期 TypeScript 项目中,使用三斜线指令来显式引用其他声明文件。现代项目中已基本被 importtsconfig.jsontypes 选项取代,但了解它有助于阅读旧代码。

TypeScript 复制代码
/// <reference path="path/to/types.d.ts" />

这告诉编译器在编译时包含 types.d.ts 文件。现在推荐使用 tsconfig.jsonincludeexclude 字段。

不过三斜线指令在某些场景仍有用途:例如引用一个全局库的类型声明,且不想通过 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.jsontypestypings 字段定位。

现代项目推荐使用 "moduleResolution": "node"(如果输出 CommonJS)或 "moduleResolution": "node16" / "bundler"(取决于你的运行环境)。

7.3 路径映射(paths

可以在 tsconfig.json 中设置 paths 来简化导入路径:

TypeScript 复制代码
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

之后可以 import utils from '@/utils'


八、命名空间(namespace

在 TypeScript 早期,模块系统尚不成熟时,namespace 用于组织代码。现在推荐使用 ES 模块,但你可能在旧项目中看到。

TypeScript 复制代码
namespace MyNamespace {
  export const version = '1.0';
  export function doWork() {}
}
​
// 使用
MyNamespace.doWork();

namespace 编译后会生成 IIFE 包裹的对象。它与 ES 模块不冲突,但新项目不建议使用。


九、全局类型扩展与模块增强

有时你需要为已有的对象或模块添加额外的属性/方法。

9.1 为全局对象添加属性

例如,想在 window 上挂载 myApp 变量:

TypeScript 复制代码
// global.d.ts
interface Window {
  myApp: {
    version: string;
  };
}

之后在任意 .ts 文件中:

TypeScript 复制代码
window.myApp = { version: '1.0' };  // 类型安全

9.2 为第三方模块扩展类型(模块增强)

假设你想为 expressRequest 对象添加 user 属性:

TypeScript 复制代码
// 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.jsoninclude 字段明确包含类型文件

TypeScript 复制代码
{
  "include": ["src/**/*", "types/**/*.d.ts"]
}

避免全局类型污染 :尽量使用模块化导出/导入,而不是 declare var

对于第三方库缺少类型的情况 ,优先 @types/,实在没有则局部声明并考虑向上游贡献。

开启 strict 模式 (包含 strictNullChecksnoImplicitAny 等),强制类型安全。


十一、总结

本文全面讲解了 TypeScript 与外部世界交互的类型层面:

  • 类型声明文件(.d.ts :为 JS 代码补充类型,理解 declare 语法。

  • 使用第三方库的类型@types/ 安装或自带类型,以及缺失时的应对方案。

  • 编写自定义声明:为全局变量、模块、CommonJS 导出等提供类型。

  • 模块系统 :ES Module 与 CommonJS 的兼容,esModuleInterop 的作用。

  • 模块解析moduleResolution 选项与路径映射。

  • 命名空间(了解即可):旧式组织代码方式。

  • 全局扩展与模块增强:为已有类型添加新成员。

  • 项目组织最佳实践:保持类型清晰、可控。

至此,你已经掌握了 TypeScript 在日常开发中所需的所有核心知识点。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

相关推荐
无聊的老谢2 小时前
Vue 3 + TypeScript 构建大型电信运维平台的前端架构设计
前端·vue.js·typescript
xiaofeichaichai2 小时前
Map / Set / WeakMap / WeakSet
前端·javascript
有梦想的程序星空4 小时前
【环境配置】Vue3项目离线化本地部署echarts全攻略
前端·javascript·vue·echarts
薛先生_0994 小时前
vue-路由重定向
前端·javascript·vue.js
橘子星5 小时前
基于 ES6 语法的 NLP 任务模块化开发实践
前端·javascript
月光刺眼5 小时前
JS 底层执行机制探讨:执行上下文、变量提升与调用栈
前端·javascript
ZC跨境爬虫6 小时前
跟着 MDN 学 JavaScript day_1:什么是 JavaScript?
开发语言·前端·javascript·ecmascript
xiaofeichaichai6 小时前
Vue 响应式原理
前端·javascript·vue.js
提子拌饭1336 小时前
模态窗鸿蒙PC Electron框架实现技术详解 - 饮料含糖量应用案例分析
前端·javascript·华为·electron·前端框架·开源·鸿蒙