Skip to content
模块与声明文件

模块与声明文件

本章涵盖 TypeScript 中的 ES 模块语法、声明文件(.d.ts)编写、全局类型扩展、模块解析策略以及命名空间的遗留用法与替代方案。


ES 模块

TypeScript 完全支持 ES Module 语法,并在此基础上增强了类型层面的导入/导出能力。

import / export 语法

// ——— Named exports ———
export function add(a: number, b: number): number { return a + b }
export const VERSION = "1.0.0"
export interface MathOp { (a: number, b: number): number }

// ——— Named imports ———
import { add, VERSION } from "./math"
import type { MathOp } from "./math"

// ——— Default export ———
export default class Calculator {
  add(a: number, b: number): number { return a + b }
}

// ——— Default import ———
import Calculator from "./calculator"

// ——— Namespace import(导入全部)———
import * as Math from "./math"
Math.add(1, 2)
Math.VERSION

// ——— Re-export ———
export { add } from "./math"
export { add as sum } from "./math"       // 重命名再导出
export * from "./math"                    // 全部重新导出
export { default as Calc } from "./calc"  // 重新导出 default

type-only imports / exports(TS 3.8+)

// 整行导入仅作为类型使用,编译后完全擦除
import type { User } from "./types"

// 混合 value + inline type import(TS 4.5+)
import { type User, getUser } from "./user"

// type-only re-export
export type { User, Admin } from "./types"

💡 最佳实践:使用 import type 确保导入仅用于类型检查,不会产生运行时依赖,在 --isolatedModulesverbatimModuleSyntax 下尤为关键,也能有效避免循环引用问题。

// ✅ 仅类型层面引用,不产生运行时依赖
import type { Config } from "./config"
const c: Config = { ... }

// ❌ 普通 import 即使只用于类型标注,也会引入运行时依赖
import { Config } from "./config"

import() 动态导入类型

// 运行时动态导入
const module = await import("./heavy-module")
module.doWork()

// 类型层面:获取模块的完整类型(TS 2.9+)
type MyModule = typeof import("./my-module")
const mod: MyModule = await import("./my-module")

声明文件 .d.ts

.d.ts 文件用于描述 JavaScript 代码的类型信息,相当于 C/C++ 中的头文件 — 只包含类型声明,不包含可执行逻辑。

基本概念

  • .d.ts 文件只能包含类型声明,不能包含可执行代码
  • 作用:让 TypeScript 理解非 TS 代码(JS 库、浏览器全局变量、CSS 模块等)的形状
  • 编译时用于类型检查,不会输出任何 JavaScript

declare 关键字

declare 告诉编译器"这个东西存在,类型如下,但不要生成任何代码"。

// declare 全局变量
declare const API_URL: string

// declare 全局函数
declare function initialize(config: Config): void

// declare 类
declare class EventEmitter {
  on(event: string, handler: (...args: any[]) => void): this
  emit(event: string, ...args: any[]): boolean
}

// declare 模块
declare module "*.css" {
  const content: Record<string, string>
  export default content
}

全局类型声明

在 script 模式文件(无 import/export 语句)中直接声明的类型会进入全局作用域:

// global.d.ts — 注意:文件中不能有 import/export,否则变成模块模式
interface Window {
  myCustomProperty: string
}

declare const __DEV__: boolean

🚨 陷阱:只要文件中出现任何顶层的 importexport,该文件就会变成模块模式,其声明的类型变为模块私有。如需在模块文件中修改全局类型,必须使用 declare global(见下文)。

declare global — 扩展全局作用域

在模块文件内部,通过 declare global 包裹来显式扩展全局类型:

// 文件中有 export,说明是模块模式
export {}

declare global {
  interface Window {
    __INITIAL_STATE__: string
  }
  namespace NodeJS {
    interface ProcessEnv {
      API_KEY: string
      DATABASE_URL: string
    }
  }
  // 扩展内置类型
  interface String {
    toCamelCase(): string
  }
}

💡 最佳实践:始终通过 declare global 扩展全局作用域,用 export {} 确保文件为模块模式。切勿依赖无 import/export 的 script 模式文件,其行为不够直观且容易污染全局。

declare module — 模块扩展与通配模块

// 扩展已有模块的类型(Module Augmentation)
declare module "express" {
  interface Request {
    user?: { id: string; role: string }
  }
}

// 声明通配模块(如非 JS/TS 资源文件)
declare module "*.svg" {
  import type { FunctionComponent, SVGProps } from "react"
  const content: FunctionComponent<SVGProps<SVGSVGElement>>
  export default content
}

declare module "*.png" {
  const src: string
  export default src
}

declare module "*.module.css" {
  const classes: { readonly [key: string]: string }
  export default classes
}

环境声明

为 JS 库编写声明文件

典型的库声明文件结构:

// types/my-lib/index.d.ts
export function doSomething(input: string): Result

export interface Result {
  code: number
  data: unknown
}

export { default as Config } from "./config"

// 如果库导出了多个子路径
export * from "./sub-module"

💡 最佳实践:声明文件应与 JS 源码同级对齐。在 package.json 中使用 typestypings 字段指定声明文件入口 — 现代工具链优先读取 exports 中的 types 条件。

// package.json
{
  "name": "my-lib",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    }
  }
}

@types 组织

DefinitelyTyped 是社区维护的类型仓库,为无自带类型的 npm 包提供类型声明。

  • @types/node — Node.js API 类型
  • @types/react — React 类型
  • @types/express — Express 类型
  • @types/jest — Jest 测试框架类型
npm i -D @types/package-name

TypeScript 会自动从 node_modules/@types 中查找类型声明,无需显式配置。


模块解析

模块解析策略

策略 说明 适用场景
classic 旧版策略,已基本弃用 遗留项目
node 模仿 Node.js 的 require() 解析逻辑 CommonJS 项目
node16 / nodenext 现代 Node.js ES Module 解析,支持 package.jsontype 字段 Node.js 原生 ESM 项目
bundler 打包器(Vite / Webpack / esbuild)解析,最宽松 前端项目

💡 最佳实践:新项目优先选择 moduleResolution: "bundler"(配合 Vite / Webpack)或 "nodenext"(Node.js 原生 ESM)。避免使用已弃用的 classic 策略。

路径别名(paths)

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

配置后即可使用简洁的绝对路径:

import { Button } from "@components/Button"
import { formatDate } from "@utils/date"
import type { User } from "@types/user"

性能提示paths 仅影响编译时的类型检查;运行时解析由打包器(webpack alias / Vite resolve.alias)或运行时(tsx / ts-node / tsconfig-paths)负责。如果运行时报错找不到模块,检查打包器是否同步配置了对应 alias。

// tsconfig.json 中 path 与 runtime 不一致时会报错
// ✅ 确保 bundler 也配置了相同 alias
// vite.config.ts
export default defineConfig({
  resolve: {
    alias: { "@": "/src" }
  }
})

namespace(命名空间)

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean
  }

  const lettersRegexp = /^[A-Za-z]+$/

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string): boolean {
      return lettersRegexp.test(s)
    }
  }
}

// 使用
const validator: Validation.StringValidator =
  new Validation.LettersOnlyValidator()

🚨 陷阱:namespace 是 TypeScript 早期的模块方案,不建议在新项目中使用。现代 TS 开发应统一使用 ES modules(import / export)。namespace 唯一仍被接受的使用场景是声明文件中扩展第三方类型(如 declare namespace Express { ... })或在 DefinitelyTyped 类型包中组织历史代码。

// ✅ 现代写法:使用 ES module
// validation.ts
const lettersRegexp = /^[A-Za-z]+$/
export class LettersOnlyValidator implements StringValidator {
  isAcceptable(s: string): boolean { return lettersRegexp.test(s) }
}

// ❌ 遗留写法:namespace 增加不必要的嵌套
namespace Validation {
  export class LettersOnlyValidator { ... }
}

三斜线指令

三斜线指令是 TypeScript 早期的类型引用方式,现在大部分功能已被 tsconfig.json 取代。

/// <reference path="./other.d.ts" />      // 引用其他声明文件
/// <reference types="node" />              // 声明对 @types/node 的依赖
/// <reference lib="es2022" />              // 声明使用的 lib

🚨 陷阱:三斜线指令大部分已过时。优先使用 tsconfig.json 中的 typeslib 字段进行配置。仅当声明文件不通过 npm 分发,需要直接引用同目录的其他 .d.ts 文件时,才使用 /// <reference path="..." />


常见陷阱汇总

陷阱 说明 正确做法
在 script 文件中意外污染全局作用域 .d.ts 文件中若没有 import/export,所有声明进入全局,冲突后报错 添加 export {} 将文件转为模块模式,用 declare global 显式声明全局类型
type-only import 被当作普通 import isolatedModules 下,仅用于类型标注的 import 也可能被保留 使用 import type 确保编译后完全擦除
namespace 残留代码 新建项目中使用 namespace 封装逻辑,增加不必要的嵌套层级 一律使用 ES module 的 import / export
模块声明冲突 多个 declare module "xxx" 对同一模块声明不相容的类型 模块扩展应合并接口而非覆盖,必要时用接口继承确保兼容
路径别名未配置 moduleResolution 配置了 pathsmoduleResolution 仍为 classic,别名不生效 moduleResolution 设为 bundlernodenext