模块与组织:代码的分而治之

目录

第 1 节 概述

前九篇的所有代码示例都在单个文件里。但真实项目——几百个文件、几千行代码——怎么组织才能不混乱?

答案是模块。每个文件是一个模块,模块之间通过 import / export 建立依赖关系。

TypeScript 沿用了 JavaScript 的 ES Module 标准,并在此基础上增加了类型导出相关的特性。

第 2 节 模块基础

一个文件就是一个模块,模块通过 export 暴露内容,通过 import 引入内容。

导出(export):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// math.ts
export function add(a: number, b: number): number {
  return a + b
}

export const PI = 3.14159

export interface Point {
  x: number
  y: number
}

导入(import):

1
2
3
4
5
// app.ts
import { add, PI, type Point } from "./math"

const p: Point = { x: 10, y: 20 }
console.log(add(PI, 0.00001))

2.1 默认导出

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

1
2
3
4
5
6
7
8
9
// config.ts
export default {
  apiUrl: "https://api.example.com",
  timeout: 5000,
}

// 导入时,名字可以任意取
import config from "./config"
// import myConfig from "./config"  也可以
技巧

团队项目中约定导出的风格更好。一些团队只用命名导出(export),因为默认导出的重命名给代码阅读造成负担。另一些团队在"一个文件一个主要功能"时用默认导出。保持一致即可。

2.2 重导出

可以集中导出一个目录下多个模块的内容:

1
2
3
4
// index.ts —— 目录的入口文件
export { User, createUser } from "./user"
export { Post, createPost } from "./post"
export type { ApiResponse } from "./types"

第 3 节 类型导入

当只导入类型时,可以加 type 关键字,编译时会被移除:

1
2
3
4
5
6
7
8
// 只导入值(编译后保留)
import { formatDate } from "./utils"

// 只导入类型(编译后消失)
import type { User, Post } from "./types"

// 混合同一个源文件
import { createUser, type User } from "./user"
说明

import type 是 TypeScript 3.8 引入的特性。它明确告诉编译器"我只需要类型声明",在某些构建工具中可以触发更优化的编译策略。TypeScript 5.0 后,如果你显式写了 type,编译器会建议你使用 import type 语法。

同样地,导出时也可以标记类型:

1
2
3
export type { User }
// 或者
export { type User, createUser }

第 4 节 模块解析

当我们写 import { add } from "./math" 时,TypeScript 怎么找到这个文件?

相对路径导入(以 ./../ 开头):解析为相对于当前文件的位置。

1
2
import { User } from "./types/user"   // 当前目录的 types/user.ts
import { Config } from "../config"    // 上级目录的 config.ts

非相对路径导入(不以 ./../ 开头):在 node_modules 中查找。

1
2
import { ref } from "vue"          // node_modules/vue
import { defineConfig } from "vite" // node_modules/vite

TypeScript 编译器自动解析文件扩展名(.ts.tsx.d.ts)和 index.ts

1
2
3
4
5
6
7
8
// 以下导入方式都有效
import { User } from "./models/user"
// 会按顺序查找:
//   ./models/user.ts
//   ./models/user.tsx
//   ./models/user.d.ts
//   ./models/user/index.ts
//   ./models/user/index.d.ts

第 5 节 路径别名

tsconfig.json 中配置路径别名,避免深层相对路径:

配置后,可以用简洁的路径导入:

1
2
3
// 不用 ../../../types/user
import { User } from "@/types/user"
import { formatDate } from "@utils/date"

第 6 节 namespace 简述

namespace 是 TypeScript 早期的模块方案(在 ES Module 标准化之前使用)。现在很少用了,但了解旧项目时需要:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace Geometry {
  export const PI = 3.14159

  export function areaOfCircle(radius: number): number {
    return PI * radius * radius
  }
}

// 使用
console.log(Geometry.PI)
console.log(Geometry.areaOfCircle(5))

namespace 也可以跨文件,通过 /// <reference> 引用。

注意

现代 TypeScript 项目应使用 ES Module(import/export),而不是 namespace。只有在你写全局声明文件(.d.ts)时,namespace 还有一席之地。

第 7 节 文件组织建议

中型 TypeScript 项目的常见目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
src/
├── types/           # 类型定义
│   ├── user.ts
│   ├── api.ts
│   └── index.ts     # 重导出所有类型
├── utils/           # 工具函数
│   ├── date.ts
│   └── format.ts
├── services/        # API 调用
│   └── userService.ts
├── models/          # 数据模型
│   └── User.ts
└── index.ts         # 应用入口

几个组织原则:

原则说明
按功能分目录类型放 types/,工具放 utils/,服务放 services/
每个文件一个主要概念user.ts 只导出 User 相关的类型和函数
目录入口 index.ts集中导出目录内容,外部只引目录路径
类型和逻辑分开纯类型定义放 types/,运行时逻辑不放类型文件

模块化让你能把千行代码组织得井井有条。

现在你有了完整 TypeScript 技能树:基础类型 → 类型运算 → 对象/接口/类 → 泛型 → 守卫 → 模块。

下一篇是真正的"进阶"篇——类型工具、映射类型、条件类型、声明文件……这些是 TypeScript 高手的武器库。

目录