跳到主要内容

TypeScript 模块

JavaScript 有多种模块化方式,TypeScript 支持其中的大多数。现在社区和 JavaScript 规范已经统一到 ES Modules(ES6 模块),即 import/export 语法。

什么是模块

在 TypeScript 中,任何包含顶层 importexport 的文件都被视为模块。相反,没有任何顶层 importexport 的文件被视为脚本,其内容在全局作用域中可用。

模块的特点

  • 模块在自己的作用域内执行,而不是全局作用域
  • 模块中声明的变量、函数、类等不会自动暴露到外部
  • 必须显式导出才能被其他模块使用
  • 必须显式导入才能使用其他模块的导出

将文件转为模块

如果文件没有 importexport,但想作为模块处理,可以添加:

export {};  // 将文件标记为模块

ES Module 语法

导出声明

使用 export 关键字导出声明:

// maths.ts

// 导出变量
export const pi = 3.14;
export let squareTwo = 1.41;
export const phi = 1.61;

// 导出函数
export function absolute(num: number): number {
return num < 0 ? num * -1 : num;
}

// 导出类
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}

// 导出接口
export interface Point {
x: number;
y: number;
}

// 导出类型别名
export type ID = string | number;

解释:任何声明(变量、函数、类、接口、类型别名)都可以通过在前面添加 export 关键字来导出。

导出语句

可以先声明再导出:

// maths.ts

const pi = 3.14;
const phi = 1.61;

function absolute(num: number): number {
return num < 0 ? num * -1 : num;
}

// 统一导出
export { pi, phi, absolute };

// 重命名导出
export { pi as PI };

默认导出

每个模块可以有一个默认导出:

// hello.ts

// 默认导出函数
export default function helloWorld(): void {
console.log("Hello, world!");
}

// 也可以导出其他声明
export const version = "1.0.0";
// 或者先声明再默认导出
function helloWorld(): void {
console.log("Hello, world!");
}

export default helloWorld;

解释:默认导出不需要名称,导入时可以任意命名。每个模块只能有一个默认导出。

导入

命名导入

// app.ts
import { pi, phi, absolute } from "./maths.js";

console.log(pi); // 3.14
console.log(absolute(-5)); // 5

重命名导入

import { pi as π } from "./maths.js";

console.log(π); // 3.14

默认导入

import helloWorld from "./hello.js";

helloWorld(); // Hello, world!

混合导入

import helloWorld, { pi, phi } from "./hello.js";

helloWorld();
console.log(pi);

导入整个模块

import * as math from "./maths.js";

console.log(math.pi); // 3.14
console.log(math.absolute(-5)); // 5

副作用导入

只执行模块代码,不导入任何内容:

import "./polyfills.js";  // 执行 polyfills.js 中的代码

重新导出

可以重新导出其他模块的内容:

// utils/index.ts

// 重新导出其他模块
export { pi, phi } from "./maths.js";
export { default as helloWorld } from "./hello.js";

// 重新导出所有内容
export * from "./constants.js";

// 重命名重新导出
export { pi as PI } from "./maths.js";

TypeScript 特有的模块语法

import type

使用 import type 只导入类型,不会在编译后的 JavaScript 中保留:

// types.ts
export type User = {
id: number;
name: string;
};

export interface Config {
apiUrl: string;
}

export const defaultConfig: Config = {
apiUrl: "https://api.example.com"
};

// app.ts
// 只导入类型
import type { User, Config } from "./types.js";

const user: User = {
id: 1,
name: "张三"
};

// 错误:createCatName 是值,不能用 import type
// import type { defaultConfig } from "./types.js";

解释import type 确保导入的内容在编译后被完全移除,避免不必要的运行时代码。

内联类型导入

TypeScript 4.5+ 支持在导入中混合类型和值:

import { createCatName, type Cat, type Dog } from "./animal.js";

// Cat 和 Dog 是类型,createCatName 是值
type Animals = Cat | Dog;
const name = createCatName();

export type

只导出类型:

// types.ts
export type User = {
id: number;
name: string;
};

export type Admin = User & {
permissions: string[];
};

// 另一种写法
type Product = {
id: number;
name: string;
};

export type { Product };

CommonJS 语法

CommonJS 是 Node.js 传统的模块格式,大多数 npm 包使用这种格式。

导出

// maths.ts

function absolute(num: number): number {
return num < 0 ? num * -1 : num;
}

module.exports = {
pi: 3.14,
squareTwo: 1.41,
phi: 1.61,
absolute,
};

导入

const maths = require("./maths");

console.log(maths.pi); // 3.14
console.log(maths.absolute(-5)); // 5

// 解构导入
const { pi, absolute } = require("./maths");
console.log(pi); // 3.14

ES Module 与 CommonJS 互操作

TypeScript 提供了 esModuleInterop 编译选项来改善两种模块系统的互操作性:

// tsconfig.json
{
"compilerOptions": {
"esModuleInterop": true
}
}

启用后可以更方便地导入 CommonJS 模块:

import fs from "fs";  // 可以直接默认导入
import { readFileSync } from "fs"; // 命名导入

import = require 语法

TypeScript 提供了一种特殊的导入语法,与 CommonJS 的 require 直接对应:

import fs = require("fs");

const content = fs.readFileSync("file.txt", "utf8");

解释:这种语法确保 TypeScript 代码与 CommonJS 输出 1:1 对应。

模块解析策略

模块解析是将 importrequire 中的字符串转换为文件路径的过程。

TypeScript 提供两种解析策略:

Classic 策略

用于向后兼容,当 module 不是 commonjs 时的默认策略。

相对路径导入./moduleB):

  1. /root/src/folder/ 目录下找到 moduleB.ts
  2. 如果找不到,找 /root/src/folder/moduleB.d.ts

非相对路径导入moduleB):

  1. /root/src/folder/ 目录下找到 moduleB.ts
  2. 如果找不到,继续向上一级目录查找

Node 策略

模拟 Node.js 的模块解析机制,推荐使用。

相对路径导入./moduleB):

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json(查看 types 字段)
  5. /root/src/moduleB/index.ts

非相对路径导入moduleB):

  1. node_modules 中查找
  2. 向上遍历目录查找 node_modules

配置模块解析

tsconfig.json 中配置:

{
"compilerOptions": {
"moduleResolution": "node",
"baseUrl": "./src",
"paths": {
"@/*": ["*"],
"@components/*": ["components/*"]
}
}
}

解释

  • moduleResolution:设置解析策略
  • baseUrl:设置模块解析的基础目录
  • paths:设置路径别名

使用路径别名:

// 之前
import { Button } from "../../../components/Button";

// 使用别名后
import { Button } from "@/components/Button";

模块输出格式

TypeScript 可以将代码编译为多种模块格式:

ES Modules (ES2020+)

// TypeScript 源码
import { pi } from "./maths.js";
export const twoPi = pi * 2;

// 编译后(几乎相同)
import { pi } from "./maths.js";
export const twoPi = pi * 2;

CommonJS

// 编译后
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.twoPi = void 0;
const maths_js_1 = require("./maths.js");
exports.twoPi = maths_js_1.pi * 2;

UMD (Universal Module Definition)

兼容 AMD、CommonJS 和全局变量的格式:

// 编译后(简化版)
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
// CommonJS
factory(require, exports);
} else if (typeof define === "function" && define.amd) {
// AMD
define(["require", "exports", "./maths.js"], factory);
}
})(function (require, exports) {
"use strict";
// ... 模块代码
});

选择输出格式

tsconfig.json 中配置:

{
"compilerOptions": {
"module": "ES2020", // 或 "CommonJS", "UMD", "AMD", "System"
"target": "ES2020"
}
}

常见组合:

  • Node.js 应用"module": "CommonJS"
  • 现代浏览器"module": "ES2020"
  • 库开发:同时提供 ES Module 和 CommonJS 两种格式

命名空间(Namespaces)

命名空间是 TypeScript 早期的模块化方案,现在推荐使用 ES Modules。

namespace Utils {
export function log(message: string): void {
console.log(message);
}

export const version = "1.0.0";

// 嵌套命名空间
export namespace Math {
export const pi = 3.14;
}
}

Utils.log("Hello"); // Hello
console.log(Utils.version); // 1.0.0
console.log(Utils.Math.pi); // 3.14

命名空间 vs 模块

特性命名空间模块
文件级别可在单文件中定义每个文件一个模块
依赖声明需要手动管理通过 import 声明
作用域全局作用域模块作用域
推荐不推荐新项目使用推荐使用

模块设计最佳实践

1. 一个模块一个职责

// user.ts - 只处理用户相关
export interface User {
id: number;
name: string;
}

export function createUser(name: string): User {
return { id: Date.now(), name };
}

// product.ts - 只处理产品相关
export interface Product {
id: number;
name: string;
price: number;
}

2. 使用 index.ts 统一导出

// models/index.ts
export { User } from "./user";
export { Product } from "./product";
export { Order } from "./order";

// 使用时
import { User, Product, Order } from "./models";

3. 避免循环依赖

// a.ts
import { b } from "./b"; // b 导入 a,a 导入 b = 循环依赖

// 解决方案:提取共享类型到单独文件
// types.ts
export interface SharedType {
// ...
}

4. 使用桶文件(Barrel Files)

// components/index.ts
export * from "./Button";
export * from "./Input";
export * from "./Select";

// 使用
import { Button, Input, Select } from "./components";

5. 类型与实现分离

// types.ts - 纯类型定义
export interface ApiClient {
get<T>(url: string): Promise<T>;
post<T>(url: string, data: unknown): Promise<T>;
}

// client.ts - 实现
import type { ApiClient } from "./types";

export class HttpClient implements ApiClient {
async get<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}

async post<T>(url: string, data: unknown): Promise<T> {
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(data)
});
return response.json();
}
}

小结

本章我们学习了 TypeScript 模块系统:

  1. ES Module 语法import/export 的各种用法
  2. TypeScript 特有语法import typeexport type
  3. CommonJS 兼容:与 Node.js 传统模块的互操作
  4. 模块解析策略:如何找到导入的模块文件
  5. 模块输出格式:编译后的不同模块格式
  6. 命名空间:旧式模块方案
  7. 最佳实践:模块设计建议

练习

  1. 创建一个包含多个模块的工具库,使用 index.ts 统一导出
  2. 使用 import type 重构现有代码,确保类型导入被正确移除
  3. 配置 paths 别名,简化深层目录的导入路径
  4. 将一个 CommonJS 模块转换为 ES Module 格式