TypeScript 模块
JavaScript 有多种模块化方式,TypeScript 支持其中的大多数。现在社区和 JavaScript 规范已经统一到 ES Modules(ES6 模块),即 import/export 语法。
什么是模块
在 TypeScript 中,任何包含顶层 import 或 export 的文件都被视为模块。相反,没有任何顶层 import 或 export 的文件被视为脚本,其内容在全局作用域中可用。
模块的特点:
- 模块在自己的作用域内执行,而不是全局作用域
- 模块中声明的变量、函数、类等不会自动暴露到外部
- 必须显式导出才能被其他模块使用
- 必须显式导入才能使用其他模块的导出
将文件转为模块
如果文件没有 import 或 export,但想作为模块处理,可以添加:
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 对应。
模块解析策略
模块解析是将 import 或 require 中的字符串转换为文件路径的过程。
TypeScript 提供两种解析策略:
Classic 策略
用于向后兼容,当 module 不是 commonjs 时的默认策略。
相对路径导入(./moduleB):
- 在
/root/src/folder/目录下找到moduleB.ts - 如果找不到,找
/root/src/folder/moduleB.d.ts
非相对路径导入(moduleB):
- 在
/root/src/folder/目录下找到moduleB.ts - 如果找不到,继续向上一级目录查找
Node 策略
模拟 Node.js 的模块解析机制,推荐使用。
相对路径导入(./moduleB):
- 找
/root/src/moduleB.ts - 找
/root/src/moduleB.tsx - 找
/root/src/moduleB.d.ts - 找
/root/src/moduleB/package.json(查看types字段) - 找
/root/src/moduleB/index.ts
非相对路径导入(moduleB):
- 在
node_modules中查找 - 向上遍历目录查找
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 模块系统:
- ES Module 语法:
import/export的各种用法 - TypeScript 特有语法:
import type、export type - CommonJS 兼容:与 Node.js 传统模块的互操作
- 模块解析策略:如何找到导入的模块文件
- 模块输出格式:编译后的不同模块格式
- 命名空间:旧式模块方案
- 最佳实践:模块设计建议
练习
- 创建一个包含多个模块的工具库,使用
index.ts统一导出 - 使用
import type重构现有代码,确保类型导入被正确移除 - 配置
paths别名,简化深层目录的导入路径 - 将一个 CommonJS 模块转换为 ES Module 格式