Skip to content

TypeScript 模块

大白话解释: TypeScript 的模块就像"文件柜"。每个文件是一个抽屉,抽屉里可以放东西(导出),也可以从别的抽屉拿东西(导入)。这样代码就不会乱成一团,每个文件负责一件事。

为什么要用模块?

  • 代码组织:把代码分成多个文件,每个文件负责一件事
  • 避免冲突:不同文件的变量不会互相污染
  • 按需加载:只导入需要的代码,减少打包体积
  • 团队协作:不同人可以同时开发不同模块

TypeScript 支持 ES 模块和 CommonJS 模块系统,提供了强大的模块化编程能力。


ES 模块

基本导入导出

ts
// ---- ES 模块:现代 JavaScript 的模块系统 ----
// 大白话:就像"文件柜",通过 import/export 打开抽屉拿东西

// 导出:把东西从抽屉里拿出来
export interface User {
  id: number
  name: string
  email: string
}

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

export class UserService {
  private users: User[] = []

  add(user: User): void {
    this.users.push(user)
  }

  findById(id: number): User | undefined {
    return this.users.find(user => user.id === id)
  }
}

// 默认导出:一个文件只能有一个
export default class App {
  // ...
}
ts
// 导入:从别的抽屉拿东西进来
import App, { User, createUser, UserService } from './user'

// 导入全部
import * as UserModule from './user'
const user = UserModule.createUser('张三', '[email protected]')

// 重命名导入
import { createUser as createNewUser } from './user'

// 仅导入类型(编译后会被删除,不会增加打包体积)
import type { User } from './user'

重新导出

ts
// ---- 重新导出:把导入的东西再导出去 ----
// 大白话:就像"转发",从 A 拿来,再给 B

// 重新导出指定内容
export { User, createUser } from './user'
export { UserService } from './user'

// 重新导出全部
export * from './user'

// 重命名重新导出
export { createUser as createNewUser } from './user'

// 默认导出重新导出
export { default as App } from './App'

// 实际用法:创建统一的导出入口
// services/index.ts
export { UserService } from './UserService'
export { OrderService } from './OrderService'
export { PaymentService } from './PaymentService'

// 使用时只需从一个地方导入
import { UserService, OrderService, PaymentService } from '@/services'

CommonJS 模块

基本语法

ts
// ---- CommonJS:Node.js 的模块系统 ----
// 大白话:就像"快递",用 require 取件,用 module.exports 寄件
// 注意:这是旧版语法,现在推荐用 ES 模块(import/export)
// 什么时候会遇到?维护老项目、写 Node.js 脚本时可能碰到

// CommonJS 导出(旧语法,用 export = 而不是 export default)
export = UserService

class UserService {
  private users: User[] = []

  add(user: User): void {
    this.users.push(user)
  }
}

// CommonJS 导入(旧语法,用 import = require 而不是 import from)
import UserService = require('./UserService')
const service = new UserService()

兼容性配置

json
// tsconfig.json
{
  "compilerOptions": {
    "module": "CommonJS",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  }
}

模块解析

相对路径导入

ts
// ---- 相对路径导入:从当前文件出发 ----
// 大白话:就像"问路",从当前位置出发,走到目标位置

// 同级目录
import { User } from './User'

// 上级目录
import { UserService } from '../services/UserService'

// 上上级目录
import { Config } from '../../config'

// 导入 JSON 文件
import config from '../config.json'

非相对路径导入

ts
// ---- 非相对路径导入:从 node_modules ----
// 大白话:就像"网购",不用知道仓库在哪,直接说商品名

// 导入第三方库
import express from 'express'
import { Request, Response } from 'express'
import lodash from 'lodash'

// 导入类型定义
import type { Express } from 'express'

路径别名

ts
// ---- 路径别名:给路径起个短名字 ----
// 大白话:就像"快捷方式",不用写长长的路径

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

// 使用
import { Button } from '@components/Button'
import { formatDate } from '@utils/date'
import { User } from '@/models/User'

// Vite 配置
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
  },
})

声明文件

全局声明

ts
// ---- 全局声明:给全局变量加类型 ----
// 大白话:就像"给窗户贴膜",让外面的变量也能被 TS 识别

// global.d.ts
declare global {
  interface Window {
    __APP_CONFIG__: {
      apiUrl: string
      env: 'development' | 'production'
    }
  }

  // 全局类型
  type Nullable<T> = T | null
  type Optional<T> = T | undefined
}

export {}

模块声明

ts
// ---- 模块声明:给没有类型的库加类型 ----
// 大白话:就像"翻译",让 TS 能理解没有类型的库

// types/express.d.ts
declare module 'express' {
  export interface Request {
    body: any
    params: Record<string, string>
    query: Record<string, string>
  }

  export interface Response {
    json(data: any): void
    status(code: number): Response
  }

  export interface Application {
    get(path: string, handler: (req: Request, res: Response) => void): void
    post(path: string, handler: (req: Request, res: Response) => void): void
    listen(port: number, callback?: () => void): void
  }

  export default function express(): Application
}

第三方库声明

ts
// ---- 第三方库声明:给没有类型的库加类型 ----
// 大白话:就像"给没有标签的瓶子贴标签"

// types/lodash.d.ts
declare module 'lodash' {
  export function debounce<T extends (...args: any[]) => any>(
    func: T,
    wait?: number
  ): T

  export function throttle<T extends (...args: any[]) => any>(
    func: T,
    wait?: number
  ): T

  export function cloneDeep<T>(value: T): T
  export function merge(...objects: any[]): any
}

// 使用
import { debounce, cloneDeep } from 'lodash'

命名空间

基本命名空间

ts
// ---- 命名空间:旧式的模块系统 ----
// 大白话:就像"文件夹",把相关的类型放在一起
// 注意:现在推荐用 ES 模块,命名空间主要用于声明文件

namespace Validation {
  export interface Validator {
    validate(value: string): boolean
  }

  export class EmailValidator implements Validator {
    validate(value: string): boolean {
      return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
    }
  }

  export class PhoneValidator implements Validator {
    validate(value: string): boolean {
      return /^1[3-9]\d{9}$/.test(value)
    }
  }
}

// 使用
const emailValidator = new Validation.EmailValidator()
const phoneValidator = new Validation.PhoneValidator()

emailValidator.validate('[email protected]')  // true
phoneValidator.validate('13800138000')       // true

命名空间合并

ts
// ---- 命名空间合并:多个地方定义同一个命名空间 ----
// 大白话:就像"拼图",把多个部分拼在一起

namespace Validation {
  export function isEmpty(value: string): boolean {
    return value.trim().length === 0
  }
}

// 合并后可以使用
Validation.isEmpty('')  // true
Validation.EmailValidator  // 也可以用

模块导出策略

默认导出 vs 命名导出

ts
// ---- 默认导出:一个文件一个主要导出 ----
// 大白话:就像"主角",一个文件只能有一个主角

// user.ts
export default class User {
  constructor(public name: string) {}
}

// 导入时可以任意命名
import User from './user'
import MyUser from './user'  // 也可以叫别的名字

// ---- 命名导出:一个文件可以有多个导出 ----
// 大白话:就像"配角",一个文件可以有多个配角

// user.ts
export class User {
  constructor(public name: string) {}
}

export interface UserConfig {
  name: string
  age: number
}

export function createUser(config: UserConfig): User {
  return new User(config.name)
}

// 导入时必须用原名
import { User, UserConfig, createUser } from './user'

选择建议

场景推荐原因
单个主要导出默认导出导入时可以任意命名
多个导出命名导出明确知道导入什么
工具函数库命名导出按需导入,支持 tree-shaking
类库默认导出通常一个文件一个主要类
类型定义命名导出类型通常需要明确导入

模块加载策略

静态导入

ts
// ---- 静态导入:编译时确定依赖 ----
// 大白话:就像"提前预约",编译时就确定要什么

import { User } from './user'
import { UserService } from './UserService'

// 优点:
// - 编译时类型检查
// - 支持 tree-shaking
// - 依赖关系明确

动态导入

ts
// ---- 动态导入:运行时加载 ----
// 大白话:就像"临时抱佛脚",需要的时候才去拿

async function loadUserModule() {
  const { User } = await import('./user')
  return new User('张三')
}

// 代码分割:按需加载组件(Vue 3 路由懒加载)
const routes = [
  {
    path: '/home',
    component: () => import('./views/Home.vue'),  // 动态导入,打包时会自动分割
  },
  {
    path: '/about',
    component: () => import('./views/About.vue'),
  },
  {
    path: '/heavy',
    // defineAsyncComponent:Vue 3 的异步组件,支持加载状态和错误处理
    component: defineAsyncComponent({
      loader: () => import('./views/HeavyComponent.vue'),
      loadingComponent: LoadingSpinner,     // 加载中显示的组件
      errorComponent: ErrorDisplay,         // 加载失败显示的组件
      delay: 200,                           // 延迟多久显示 loading(防止闪烁)
      timeout: 10000,                       // 超时时间
    }),
  },
]

// 条件加载:只在开发环境加载
if (import.meta.env.DEV) {
  const { DevTools } = await import('./DevTools')
  DevTools.init()
}

模块配置

tsconfig.json 模块配置

json
{
  "compilerOptions": {
    // 模块系统:决定 import/export 编译成什么格式
    // "ESNext":现代浏览器/Vite 项目用这个,保持 import/export 原样
    // "CommonJS":Node.js 旧项目用,编译成 require/module.exports
    // "AMD"/"UMD":旧版浏览器兼容方案,现在很少用
    "module": "ESNext",

    // 模块解析策略:决定 import './foo' 怎么找到文件
    // "node":Node.js 风格,先找 foo.ts,再找 foo/index.ts,最后找 node_modules
    // "bundler":Vite/Webpack 项目推荐,比 node 更宽松,支持路径别名
    // "node16"/"nodenext":新版 Node.js,强制写扩展名
    "moduleResolution": "bundler",

    // 允许导入 JSON 文件:import config from './config.json'
    "resolveJsonModule": true,

    // 生成 .d.ts 声明文件:让其他项目能用你的类型
    "declaration": true,

    // 允许合成默认导入:import React from 'react'(即使 react 没有 default export)
    "allowSyntheticDefaultImports": true,

    // ES 模块互操作:让 CommonJS 模块可以 default import
    // 比如 import express from 'express'(express 是 CommonJS 模块)
    "esModuleInterop": true,

    // 强制文件名大小写一致:import './User' 不能匹配 user.ts
    "forceConsistentCasingInFileNames": true
  }
}

Vite 配置

ts
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
      '@components': path.resolve(__dirname, 'src/components'),
      '@utils': path.resolve(__dirname, 'src/utils'),
    },
  },
})

实际应用示例

工具函数库

ts
// ---- 工具函数库:把常用的函数放在一起 ----
// 大白话:就像"工具箱",需要什么工具就拿什么

// utils/date.ts
export function formatDate(date: Date, format: string): string {
  const year = date.getFullYear()
  const month = String(date.getMonth() + 1).padStart(2, '0')
  const day = String(date.getDate()).padStart(2, '0')
  
  return format
    .replace('YYYY', String(year))
    .replace('MM', month)
    .replace('DD', day)
}

export function parseDate(dateString: string): Date {
  return new Date(dateString)
}

export function isToday(date: Date): boolean {
  const today = new Date()
  return date.toDateString() === today.toDateString()
}

// utils/string.ts
export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1)
}

export function camelCase(str: string): string {
  return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase())
}

export function kebabCase(str: string): string {
  return str.replace(/[A-Z]/g, char => `-${char.toLowerCase()}`)
}

// utils/index.ts(统一导出入口)
export * from './date'
export * from './string'

// 使用
import { formatDate, capitalize } from '@/utils'

API 服务模块

ts
// ---- API 服务模块:封装网络请求 ----
// 大白话:就像"快递公司",负责发送和接收请求

// services/api.ts
export class ApiService {
  private baseUrl: string

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl
  }

  async get<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`)
    return response.json()
  }

  async post<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    })
    return response.json()
  }
}

// services/user.ts
import { ApiService } from './api'

export class UserService {
  private api: ApiService

  constructor(api: ApiService) {
    this.api = api
  }

  async getUsers(): Promise<User[]> {
    return this.api.get<User[]>('/users')
  }

  async createUser(user: Omit<User, 'id'>): Promise<User> {
    return this.api.post<User>('/users', user)
  }
}

// services/index.ts(统一导出入口)
export { ApiService } from './api'
export { UserService } from './user'

// 使用
import { ApiService, UserService } from '@/services'

const api = new ApiService('https://api.example.com')
const userService = new UserService(api)

组件模块

vue
<!-- ---- 组件模块:把组件相关的东西放在一起 ---- -->
<!-- 大白话:就像"零件盒",每个盒子里装一个零件的所有配件 -->

<!-- components/Button/Button.vue -->
<script setup lang="ts">
// 定义 Props 类型
export interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger'  // 按钮样式
  size?: 'small' | 'medium' | 'large'           // 按钮大小
  disabled?: boolean                             // 是否禁用
}

// 带默认值的 Props
const props = withDefaults(defineProps<ButtonProps>(), {
  variant: 'primary',
  size: 'medium',
  disabled: false,
})

// 定义事件类型
const emit = defineEmits<{
  click: [event: MouseEvent]
}>()
</script>

<template>
  <button
    :class="['btn', `btn-${variant}`, `btn-${size}`]"
    :disabled="disabled"
    @click="emit('click', $event)"
  >
    <slot />
  </button>
</template>
ts
// components/Button/index.ts(统一导出入口)
export { default as Button } from './Button.vue'
export type { ButtonProps } from './Button.vue'

// 使用
import { Button } from '@/components/Button'
import type { ButtonProps } from '@/components/Button'

常见问题

循环依赖

ts
// ---- 循环依赖:A 依赖 B,B 依赖 A ----
// 大白话:就像"鸡生蛋、蛋生鸡",互相依赖

// 问题:循环依赖
// a.ts
import { b } from './b'
export const a = 1

// b.ts
import { a } from './a'
export const b = 2

// 解决方案 1:提取公共模块,打破循环
// 将互相依赖的部分移到第三个模块
// types.ts
export interface UserData { id: number; name: string }
export interface OrderData { userId: number; product: string }

// a.ts
import type { OrderData } from './types'
export function createOrder(order: OrderData) { /* ... */ }

// b.ts
import type { UserData } from './types'
export function getUser(id: number): UserData { /* ... */ }

// 解决方案 2:延迟导入
// a.ts
export const a = 1
export function getB() {
  return import('./b').then(m => m.b)
}

模块类型定义缺失

ts
// ---- 第三方库没有类型定义 ----
// 大白话:就像"没有标签的瓶子",不知道里面是什么

// 问题:第三方库没有类型定义
import someLib from 'some-lib'  // ❌ 类型错误

// 解决方案 1:安装类型定义(推荐)
// npm install --save-dev @types/some-lib

// 解决方案 2:创建声明文件
// types/some-lib.d.ts
declare module 'some-lib' {
  export function someFunction(): void
  export default someLib
}

// 解决方案 3:使用 any(不推荐)
declare module 'some-lib' {
  const someLib: any
  export default someLib
}

导入路径问题

ts
// ---- 导入路径问题 ----
// 大白话:就像"找不到路",路径写错了

// 问题:路径解析失败
import { User } from './models/User'  // ❌ 找不到模块

// 解决方案 1:检查文件扩展名
import { User } from './models/User.ts'

// 解决方案 2:配置路径别名
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

// vite.config.ts
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
})

// 使用
import { User } from '@/models/User'

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准