第 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 / export | require / 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.json 的 module 和 moduleResolution:
{
"compilerOptions": {
// 前端项目:写 ESM,交给打包器
"module": "esnext",
"moduleResolution": "bundler",
// Node 项目:跟随 package.json 的 type 字段
// "module": "nodenext",
// "moduleResolution": "nodenext"
}
}
moduleResolution | 适用 | 特点 |
|---|---|---|
bundler | Vite/webpack 等打包器 | 不强制文件扩展名、允许各种 import 写法 |
node16 / nodenext | 纯 Node 项目 | 严格区分 ESM/CJS,扩展名规则严格 |
node10(旧) | 遗留项目 | 老 Node 解析规则 |
moduleResolution: node(node10)下 import ... from './math.js' 可能报错——因为旧解析不认 .js 结尾的 ESM 导入。现代项目请用 bundler 或 nodenext。
9.2 类型从哪里来:解析的三种来源
一个 import 'some-lib' 的类型,可能来自三个地方(按优先级):
- 包自带的类型:
some-lib的package.json里types/exports字段指向的.d.ts @types/some-lib:包没带类型时,由 DefinitelyTyped 社区提供- 你自己补:
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 的类型
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 写的,只需在 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' // ✅ 现在有类型了
给第三方库打全局补丁(扩展 Window、ProcessEnv、String 等内置类型)时。这是「接口声明合并」(第 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 global 给 window 加两个自定义属性(比如 __APP_CONFIG__ 和一个埋点函数),在项目的两处代码里读写它们,确认类型提示存在。
项目练习 5 · 对比 moduleResolution
同一个 import './math.js' 的代码,分别用 moduleResolution: bundler 和 nodenext 编译,记录报错差异,写下一段「为什么前端项目推荐 bundler」的结论。