模块与声明文件
本章涵盖 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确保导入仅用于类型检查,不会产生运行时依赖,在--isolatedModules或verbatimModuleSyntax下尤为关键,也能有效避免循环引用问题。
// ✅ 仅类型层面引用,不产生运行时依赖
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🚨 陷阱:只要文件中出现任何顶层的
import或export,该文件就会变成模块模式,其声明的类型变为模块私有。如需在模块文件中修改全局类型,必须使用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中使用types或typings字段指定声明文件入口 — 现代工具链优先读取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-nameTypeScript 会自动从 node_modules/@types 中查找类型声明,无需显式配置。
模块解析
模块解析策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
classic |
旧版策略,已基本弃用 | 遗留项目 |
node |
模仿 Node.js 的 require() 解析逻辑 |
CommonJS 项目 |
node16 / nodenext |
现代 Node.js ES Module 解析,支持 package.json 中 type 字段 |
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中的types、lib字段进行配置。仅当声明文件不通过 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 |
配置了 paths 但 moduleResolution 仍为 classic,别名不生效 |
将 moduleResolution 设为 bundler 或 nodenext |