跳到主要内容

第 9 章 · 模块化与声明文件

读完本章你会
  • 理解 ESM 与 CommonJS 在 TypeScript 里的类型解析差异
  • 理解 moduleResolution 如何决定 import 找到谁
  • 看懂 .d.ts 声明文件:它是什么、装在哪、怎么发布
  • 会用 @types / declare module 给没类型的第三方库补类型
  • 会用 declare global 扩展全局类型(比如给 window 加自定义属性)

9.1 模块系统:ESM 与 CommonJS

9.1.1 两种模块系统

JavaScript 的模块系统有两大流派:

ESM(ES Modules)CommonJS(CJS)
语法import / exportrequire / module.exports
谁在用现代前端、浏览器、Node 新项目老 Node 生态、部分 npm 包
静态分析✅ 可静态分析(tree-shaking 靠它)❌ 运行时决定
// ESM
import { add } from './math.js'
export const result = add(1, 2)

// CommonJS
const { add } = require('./math')
module.exports = { result: add(1, 2) }

9.1.2 TypeScript 怎么知道用哪种

tsconfig.jsonmodulemoduleResolution

{
"compilerOptions": {
// 前端项目:写 ESM,交给打包器
"module": "esnext",
"moduleResolution": "bundler",
// Node 项目:跟随 package.json 的 type 字段
// "module": "nodenext",
// "moduleResolution": "nodenext"
}
}
moduleResolution适用特点
bundlerVite/webpack 等打包器不强制文件扩展名、允许各种 import 写法
node16 / nodenext纯 Node 项目严格区分 ESM/CJS,扩展名规则严格
node10(旧)遗留项目老 Node 解析规则
一个常见报错

moduleResolution: node(node10)下 import ... from './math.js' 可能报错——因为旧解析不认 .js 结尾的 ESM 导入。现代项目请用 bundlernodenext


9.2 类型从哪里来:解析的三种来源

一个 import 'some-lib' 的类型,可能来自三个地方(按优先级):

  1. 包自带的类型some-libpackage.jsontypes / exports 字段指向的 .d.ts
  2. @types/some-lib:包没带类型时,由 DefinitelyTyped 社区提供
  3. 你自己补declare module 'some-lib'(见 9.4)
# 库本身没类型时,装社区的类型包
npm i -D @types/node # Node 全局 API
npm i -D @types/lodash # lodash 的类型
npm i -D @types/react # React 的类型
@types 的命名规则

some-lib 的类型包叫 @types/some-lib。装了它,import 'some-lib' 时 TS 自动找到类型。装类型包用 -D(只在开发期需要)。


9.3 声明文件 .d.ts

9.3.1 它是什么

.d.ts 是「只有类型、没有实现」的声明文件。它告诉 TS「这个模块/全局存在,长这样」,但不包含可运行代码

// math.d.ts
export function add(a: number, b: number): number
export function multiply(a: number, b: number): number

编译时 TS 用 .d.ts 做类型检查,运行时用对应的 .js 干活。declare 关键字就是这个文件的语法核心:

declare function add(a: number, b: number): number
declare const PI: number
declare class Logger { log(msg: string): void }

declare 的意思是「我声明这个东西存在,但你不需要知道它怎么来的」。

9.3.2 发布一个带类型的库

给 npm 库带类型,两种主流方式:

// 方式一:和 JS 一起发布(源码用 TS 编译成 JS + 类型)
{
"name": "my-lib",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}

// 方式二:单独发 .d.ts(纯类型库)
{
"name": "my-types",
"types": "./index.d.ts"
}
源码写 TS 的话

如果你的库本身就是用 TS 写的,只需在 tsconfig.json 里开 declaration: true,编译时自动生成 .d.ts——不用手写。


9.4 给没类型的库补类型

9.4.1 最快的兜底:declare module

库没类型、@types 也没有,你可以自己声明:

// 在项目里建一个 global.d.ts
declare module 'legacy-chart' {
export function render(el: HTMLElement, data: unknown[]): void
export const version: string
}

这样 import { render } from 'legacy-chart' 就有类型了。

别用 any 敷衍

declare module 'xxx' { const x: any } 能过,但等于把类型检查关掉。尽量补具体的签名——哪怕只有几个字段,也比 any 强。

9.4.2 类型化非 JS 模块

CSS、图片这类导入,TS 默认不认识,需要环境声明:

// env.d.ts —— 常见于 Vite 项目
declare module '*.css'
declare module '*.module.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.svg' {
const src: string
export default src
}

装了 Vite 的话,vite/client 类型已经帮你声明好了这些,不需要手写。


9.5 全局类型与 declare global

9.5.1 环境声明与全局类型

.d.ts 文件里不带 import/export 时,它是全局声明——里面的类型对全项目可见:

// globals.d.ts(无 import/export)
interface Window {
// 见下:扩展 window
}
type Env = 'development' | 'production'

9.5.2 在模块里扩展全局:declare global

如果声明文件里带了 import(变成模块),想再碰全局,要用 declare global 包一层:

// 给 window 加自定义属性
declare global {
interface Window {
__APP_VERSION__: string
gtag: (event: string, params: Record<string, unknown>) => void
}
}

export {}
// 使用处
window.__APP_VERSION__ = '1.0.0' // ✅ 现在有类型了
什么时候用 declare global

给第三方库打全局补丁(扩展 WindowProcessEnvString 等内置类型)时。这是「接口声明合并」(第 3 章)在全局的用法。


本章小结

  • ESM / CommonJS 是两套模块系统;moduleResolution 决定 TS 怎么找类型
  • 类型来源三选一:包自带 .d.ts@types/* → 自己 declare module
  • .d.ts 是「只有类型没有实现」的声明文件;TS 源码开 declaration 自动生成
  • 没类型的库用 declare module 补,尽量写具体签名
  • 全局类型用无 import.d.ts;模块内扩展全局用 declare global

本章练习

本章是「项目实践章」

这一章没有 type-challenges 挑战题,改成 5 个贴近真实工程的项目练习。做完它们,你对「类型从哪来」会有实感。

项目练习 1 · 给库写声明

找一个你没用过、且没有类型的小 npm 库(或自己造一个「假库」),为它手写一份 .d.ts,至少覆盖 2 个函数 + 1 个类 + 1 个常量。验证 import 后类型提示正确。

项目练习 2 · 类型化资源模块

在项目里建 env.d.ts,声明 *.module.css*.png 的导入类型,然后实际导入一个 CSS Module / 图片,确认不再报「找不到模块的声明」。

项目练习 3 · declare module 补类型

选一个确实没有 @types 的库,用 declare module '库名' 补上你用到的那几个 API 的类型(不要用 any 全糊),并在代码里用起来。

项目练习 4 · 扩展全局

declare globalwindow 加两个自定义属性(比如 __APP_CONFIG__ 和一个埋点函数),在项目的两处代码里读写它们,确认类型提示存在。

项目练习 5 · 对比 moduleResolution

同一个 import './math.js' 的代码,分别用 moduleResolution: bundlernodenext 编译,记录报错差异,写下一段「为什么前端项目推荐 bundler」的结论。