模块、脚本与声明文件 — 作用域的两个世界与 .d.ts 的来历¶
这一章讲两件事: 你的 .ts 文件活在哪个作用域(模块的局部世界 vs 脚本的全局世界), 以及 .d.ts 声明文件从哪来、给谁看、怎么写。 读完你会理解三个日常谜团的根源:为什么删一行 export 全局都乱套、 为什么第三方库有的要装 @types、为什么「把类型放进 .d.ts」是个坏主意。
1. 顶层全景¶
每个 .ts 文件,TypeScript 先问一句:你是模块还是脚本?
有 import/export → 模块(局部作用域,不导出即私有)
没有 → 脚本(全局作用域,人人可见)
判定开关:tsconfig 的 moduleDetection(auto 猜 / force 全是模块)
(「开关」= tsconfig 编译选项,正式名 compiler option;第 15 章已正名)
声明文件(.d.ts)= 只有类型、没有实现的「影子文件」,两大用途:
① 给纯 JavaScript 代码配上类型(按同名规则匹配)
② 给全局作用域注入类型(declare / declare global / declare module)
图说:主线例子是 DEFAULT_VOLUME 常量的两次命运(有 export 和没有),
然后是 musicPlayer.js 的「影子」musicPlayer.d.ts。
这一章的两个主题其实互为镜像:模块/脚本解决「我自己的代码住哪」, 声明文件解决「别人的代码(以及运行环境)的类型从哪来」。 前者是作用域纪律,后者是类型供给链;两者合起来,就是 TypeScript 项目 「依赖从哪来、往哪去」的完整地图。第 15 章的 tsconfig 会把这张地图 固化为配置。
2. 模块与脚本:一个常量的两种命运¶
解决什么问题: 多个文件怎么共享代码而不撞名。TypeScript 的答案分两派:
模块 = 局部作用域。 有 import/export 的文件是模块:里面的变量、函数、类型, 不导出就不存在(于其它文件)1:
// constants.ts
const DEFAULT_VOLUME = 90;
// index.ts
console.log(DEFAULT_VOLUME); // ❌ 红线:找不到
// index.ts
import {DEFAULT_VOLUME} from "./constants";
console.log(DEFAULT_VOLUME); // ✅ 90
脚本 = 全局作用域。 没有任何 import/export 的文件是脚本:里面的声明
自动属于全局,任何文件直接用,不用导入——就像老式 JavaScript 把
<script> 标签一个接一个塞进 HTML2。把上面 constants.ts 的 export 删掉,
index.ts 不导入也能读到 DEFAULT_VOLUME。
把常量的两种命运并排走完:
命运 A(有 export,constants.ts 是模块):
constants.ts: export const DEFAULT_VOLUME = 90;
index.ts: console.log(DEFAULT_VOLUME); ❌ 找不到
index.ts: import {DEFAULT_VOLUME} from "./constants";
console.log(DEFAULT_VOLUME); ✅ 90
——语义:DEFAULT_VOLUME 是 constants.ts 的私有财产,带走要打招呼
命运 B(删掉 export,constants.ts 变成脚本):
constants.ts: const DEFAULT_VOLUME = 90;
index.ts: console.log(DEFAULT_VOLUME); ✅ 90,不需要 import
——语义:DEFAULT_VOLUME 成了全球公共财产
两行代码的差别(一个 export 关键字),作用域语义天差地别—— 这就是「文件身份」这个隐形开关的力量12。
为什么会这样猜? 因为 TypeScript 比 ES 模块标准还老。它诞生的年代,
import/export 还没标准化,TypeScript 代码大多以脚本形式跑——
「按有没有 import/export 猜」是那个年代的合理默认,一直保留到今天3。
而真实决定模块还是脚本的,从来不是 TypeScript,是运行环境:
浏览器里 <script type="module"> 就是模块,去掉 type="module" 就是脚本4。
原书对这段历史的收束句值得记住:「这些日子,你写的代码 99% 会是模块」—— 所以猜测机制的现代意义主要是制造意外,而不是提供便利。 理解了「这是历史默认,不是当前最优」,moduleDetection: force 就不再是 一个可选项,而是一项修正——把 2012 年的默认改成 2026 年的现实40。
先把「谁说了算」这张图钉牢:
TypeScript 编译期:按 import/export 猜文件身份(决定作用域和报错)
│
│ 但真正的身份裁决权在运行环境:
▼
浏览器:<script type="module"> = 模块 / 普通 <script> = 脚本
Node.js:.js 默认 CommonJS;ESM 要 .mjs 或 package.json 的 type: "module"
打包器:按其模块解析规则(Vite/webpack)再套一层
编译期的猜测和运行时的裁决可能不一致——文件没写 export,TypeScript 当脚本(全局作用域),而打包器按 ESM 处理(模块作用域)。多数时候没事 (没人真的依赖那个全局),但「用了全局变量却忘了它是脚本声明的」这类隐式 依赖,正是 moduleDetection: force 要消灭的东西4。
一场经典事故:Cannot redeclare block-scoped variable 'name'¶
这个自动猜测在 2026 年的日常里会咬人。新建 utils.ts,写一行:
你没声明过 name,凭什么说重复?——因为 utils.ts 没有 import/export,
被当成了脚本,name 落进了全局作用域;而全局作用域里已经有一个
DOM 声明的 name 变量(window.name,给超链接和表单设目标的那个)5。
修复方法奇特得可爱:在文件末尾加一个空的 export:
export {} 让文件变成模块,name 回到模块局部作用域,与全局的 window.name
井水不犯河水6。这场事故是「所有文件都该是模块」的最好论据——
TypeScript 也给了正式开关:
moduleDetection 有三个值:auto(默认,靠猜)、force(全部当模块)、
legacy(只用于兼容老版本,可无视)。force 之后,所有文件都必须
import/export 才能互通,「意外的全局变量」这一整类事故消失7。
迁移到 force 的代价也要说清:存量代码里若有人依赖「脚本的隐式全局」 (跨文件直接用某个未导入的常量),force 一开,全部爆红。这是好事—— 每一道新红线都是一个此前看不见的隐式依赖——但迁移要安排节奏: 先开 force,逐个把爆红处改成显式 import(IDE 的 quick fix 一键完成), 隐式依赖的清单自动浮现。这个「开关即审计」的模式,是 tsconfig 一类 严格开关的共同用法:先打开,再把报错当待办。 第 15 章的推荐配置里,这一项是基线成员。
模块判定在日常报错里的三个影子¶
「文件是模块还是脚本」平时隐身,一旦出事就以三种面貌出现:
面貌一:Cannot redeclare block-scoped variable 'X'
→ 文件被当脚本,X 撞了全局(多半是 DOM 的 window.*)。
解:export {} 或 moduleDetection: force。
面貌二:我在 A 文件声明的类型,B 文件「自动」能用
→ A 文件没有 import/export,是脚本,声明全落全局。
看似方便,实则是隐式依赖——删了 A,B 静默坏。
面貌三:import 一个文件报「找不到模块」,
但文件明明存在
→ 多半是解析配置问题(第 15 章),不是模块/脚本问题;
但排查时先确认被导入文件自身是不是脚本
(脚本没有导出,自然无可导入)。
三种面貌的共同根源都是同一个判定规则。把「先问文件是模块还是脚本」 加进排错流程的开头,能省掉这一类问题的一半时间。原书那个「99 percent of the code you'll be writing will be in modules」的判断40, 就是 force 开关的存在理由——既然事实上全是模块,就让编译器别猜了。
判断(我们的,不是书里的):
moduleDetection: "force"是 2026 年项目的 无争议默认——它和strict一样属于「不打开就是负资产」的配置。 值得想清楚的是为什么这一节能单独成章:它展示了「语言早期决定」的长尾成本—— 一个 2012 年合理的猜测(auto),在模块成为绝对主流之后变成了每个新手的 第一个坑(name 撞车)。语言设计里「默认值」的寿命最长、改起来最贵, 这是又一个样本。 如果错,会错在: 少数场景(快速脚本、单文件 demo、JS/HTML 混合的老项目) 里脚本模式反而顺手;force 一刀切后这些文件都要补 export {}—— 但即便如此,「显式补一句」也比「隐式全局」更诚实,我们维持推荐。
3. 声明文件:给 JavaScript 配一张「类型影子」¶
解决什么问题: 项目里有一段纯 JavaScript(比如老代码、别的团队写的), 你想继续让它留在 JavaScript 里,但 TypeScript 文件要用它:
// musicPlayer.js —— 纯 JavaScript
export const playTrack = (track) => {
console.log(`Playing: ${track.title}`);
};
// app.ts
import {playTrack} from "./musicPlayer";
// ❌ Could not find a declaration file for module './musicPlayer'.
报错说得很白:TypeScript 对这个 .js 文件没有任何类型信息。 解法是给 JavaScript 配一张「类型影子」——同名、.d.ts 结尾的声明文件:
// musicPlayer.d.ts
export function playTrack(track: {
title: string;
artist: string;
duration: number;
}): void;
注意这张影子只有类型,没有实现:函数体一行都没有,只有签名。 对比感受一下「有影子」和「没影子」的 IDE 体验差别:
没影子:import {playTrack} from "./musicPlayer"; ← 红线
悬停 → Could not find a declaration file for module './musicPlayer'.
就算强行忽略,playTrack(track) 传什么都行——纯 any 待遇
有影子:import {playTrack} from "./musicPlayer"; ← 干净
playTrack({title: "Otha Fish", artist: "The Pharcyde", duration: 322}); ✅
playTrack({title: 123}); ❌ title 必须是 string
悬停 playTrack → 完整签名,参数有补全
有了它,app.ts 的导入错误消失,playTrack 获得了完整的检查与补全8。 声明文件里也能导出接口和类型,供其它文件复用9。
但原书立刻敲了警钟:声明文件不会被拿来和它描述的 JavaScript 对照检查。 你在 .d.ts 里把 playTrack 误写成 playTRACK,TypeScript 一声不吭—— 两张皮,各说各话10。所以「手写声明文件描述自己的 JS」是易错且通常 不推荐的做法;它是给「无法修改的 JavaScript」准备的工具,不是日常路径。
把这张影子的「身份」再想清楚一层:musicPlayer.d.ts 因为有 export, 自己是模块——它的导出与 musicPlayer.js 的导出按「同名配对」关联, 靠的是模块解析规则,不是任何显式绑定。这也解释了为什么声明文件里 可以导出 interface 和 type:影子文件是模块,模块导出什么都行9。 如果影子文件没有 export,它就变成脚本,声明落全局——下一节的主角。
4. 全局作用域与 declare 全家桶¶
声明文件的第二用途:给全局加类型。规则和第 2 节一致——
.d.ts 文件同样按有没有 export 分模块/脚本;没有 export 的 .d.ts 是脚本,
里面的声明直接落进全局。注意 moduleDetection: force 管不到这里:
对 .d.ts 文件,moduleDetection 永远是 auto11。
declare:无中生有的变量¶
场景:页面上用 <script src="/music-api.js"></script> 引入了一个全局变量
ALBUM_API(老的第三方脚本,你没有它的类型)。declare 关键字可以
「声明一个不存在实现的值」:
// musicApi.d.ts —— 无 export,脚本,全局生效
type Album = { title: string; artist: string; releaseDate: string; };
declare const ALBUM_API: {
getAlbumInfo(upc: string): Promise<Album>;
searchAlbums(query: string): Promise<Album[]>;
};
从此全项目都能用 ALBUM_API,还有补全12。
把这段声明的「形状」记一下,后面所有 declare 都是它的变体:
declare + (const | var | let | function | class | enum) + 名字 + 类型
语义:在类型世界里,「这里存在一个叫 X 的东西,形状如下」——
不分配内存、不产生代码、编译后整个消失。
它的价值恰恰在于「消失」:ALBUM_API 的真实实现是别人那个 <script> 提供的, TypeScript 只负责在编译期相信你说的形状;如果形状说错了, 后果就是第 5 章那句 type faith——所以写 declare 时, 悬停参考的应该是真实脚本的真实 API,而不是想象中的 API。
declare 的位置决定作用域: 把这段挪进一个有 export 的 .ts 模块文件里,ALBUM_API 就只在该模块内可见 ——「我知道这个全局存在,但只有我这个文件用」的局部声明13。
declare 的家族不止 const:declare var、declare let、declare function,
甚至 enum、class、class 字段都行——共同点是只声明、不实现14。
TypeScript 管这类「没有实现的世界」叫 ambient(环境):声明文件里的一切
默认是 ambient,所以在 .d.ts 里写实现会直接报错
(An implementation cannot be declared in ambient contexts)15。
原书在这里埋了一个细节,值得单独点名:这个报错只在 skipLibCheck: false 时 才出现。第 5 节会讲到,skipLibCheck 打开后所有声明文件(包括你刚写错的这个) 都不再被检查——于是「声明文件不能含实现」这条规则,在你自己的 .d.ts 上 可能根本不会被报告。这不是规则的例外,而是检查范围的副作用: skipLibCheck 的「不查」是文件级的一刀切,不区分文件是谁写的。 把这两个知识点连起来,你就理解了为什么第 6 节要说「自写声明要薄」—— 厚声明文件在 skipLibCheck 下是零护栏的1527。
declare global:从模块里向全局喊话¶
如果全局类型想跟使用它的代码住一起,declare global 可以从模块内部开口子:
// musicUtils.ts(有 export,是模块)
export function getAlbumTitle(upc: string) { /* 用了 ALBUM_API */ }
declare global {
const ALBUM_API: {
getAlbumInfo(upc: string): Promise<Album>;
searchAlbums(query: string): Promise<Album[]>;
};
}
一个细节:declare global 块内不能再写 declare——原书给了一个 「报错先行」的示范,值得照着走一遍:
declare global {
declare const ALBUM_API: { /* ... */ };
//^^^^^^^ 红线:A 'declare' modifier cannot be used in an already ambient context.
}
报错的意思是:declare global 块本身已经 ambient,里面的声明天然 「无实现」,再叠一个 declare 是冗余。把 declare 删掉:
declare global {
const ALBUM_API: {
getAlbumInfo(upc: string): Promise<Album>;
searchAlbums(query: string): Promise<Album[]>;
};
}
错误消失,ALBUM_API 进全局16。这个「报错教你规则」的循环, 是 ambient 这个概念最好的教学现场:ambient = 无需声明「无实现」的上下文, .d.ts 文件整体是,.d.ts 之外的 declare global 块也是。
declare module:给模块(或文件类型)发类型¶
第三种用法:给没有类型定义的模块补类型:
// duration-utils.d.ts
declare module "duration-utils" {
export function formatDuration(seconds: number): string;
}
之后 import {formatDuration} from "duration-utils" 通过。
同样只声明不实现,也不会和真实模块对照——要手工保持同步17。
完整的消费体验走一遍:没有这张声明文件时,
import {formatDuration} from "duration-utils" 报
「找不到声明文件」,并且就算你无视红线,formatDuration 也是 any 待遇——
参数数量、返回值,什么都管不了。声明文件落地后,同一个导入获得
完整签名:formatDuration(309) 有补全、传错类型有红线、
悬停显示 (seconds: number) => string。
注意一个心智转变:你写的这张声明,读者不只是编译器,
还有三个月后接手的人——把每个参数名、返回值类型写清楚,
就是这份文件里唯一存在的「实现」。
declare module 还有一个进阶形态值得知道:模块名支持通配符,
第 6 节给 .png 发类型就是用它;而当你要描述「一个包里的某个子路径」时,
也可以精确到路径(declare module "lodash/debounce")。
原书没展开这些变体,但机制相同——declare module 的本质是
给模块解析加一个「类型先到」的旁路,旁路的一切便利与风险
(不对照、要手动同步)都不变17。
增广还是覆盖:一行 export {} 的天平¶
declare module 有个微妙的两面性,取决于文件是模块还是脚本:
// express.d.ts —— 文件底部有 export {}
declare module "express" {
export interface MyType { hello: string; }
}
export {};
模块态(有 export {}):declare module 是「增广」——往已有的 express 模块 上追加新类型,原有导出原封不动18。
// express.d.ts —— 删掉了 export {}
declare module "express" {
export interface MyType { hello: string; }
}
脚本态(无 export):declare module 是「覆盖」——express 模块的类型
被你整个替换,现在它只剩 MyType;import {express} from "express" 反而报错19。
一行之差,语义天翻地覆。覆盖模式偶尔有用(第三方库类型全错、整体重写), 但多数时候你想要的是增广。记住这个天平,诊断「我给库补的类型怎么把整个库 搞没了」时它就是答案。(同一行为在普通 .ts 文件里也成立—— moduleDetection: auto 时,把 express.d.ts 改名 express.ts 效果相同20。)
增广还是覆盖:一个可以当场做的判定实验¶
这个两面性值得亲手验证一次,因为它太反直觉:
实验设置:项目里装了 express(@types/express 已带类型)。
第 1 步:建 express.d.ts,写:
declare module "express" {
export interface MyType { hello: string; }
}
export {} ← 有这行:模块态
结果:import express from "express" ✅(原有导出都在)
import {MyType} from "express" ✅(新增的也能取)
第 2 步:删掉 export {}
结果:import express from "express" ❌(整个模块被覆盖,只剩 MyType)
覆盖态下,原模块的所有类型消失——这就是为什么「给库补个类型却把库搞没」 会成为现实事故。诊断口诀:「库的类型突然全丢了」→ 查 declare module 的 文件里是不是少了 export {}(或多了,取决于你想要哪一边)。1819
5. 你每天都在用的声明文件¶
「声明文件」听起来小众,其实每个项目都有几百个。三层来源:
第一层:TypeScript 自带的 JavaScript 描述。 [1, 2, 3].map(...) 为什么合法、
.transform 为什么不合法?因为 TypeScript 自带一摞 lib.*.d.ts 描述每个版本的
JavaScript:对 .map 用 Go to Definition,会跳进 lib.es5.d.ts(ES5,2009 年,
map 出生的版本)的 interface Array<T>;对 .replaceAll 会跳进
lib.es2021.string.d.ts21。tsconfig 的 lib 选项决定把哪些版本装进项目:
"lib": ["es2022"] 拿到 ES2022 及之前的一切;不指定时 lib 继承 target22。
注意这两件事是分离的:target 管编译产物(把新语法翻译成什么年代的 JS),
lib 管类型供给(编译期认识哪些 API)。把 target 设成 es5 而 lib 设成
es2022,是合法且有场景的组合(给老浏览器产出代码,但开发时享受新 API 的
类型与补全——前提是你自己准备了 polyfill,类型不会替你补运行时)。
反过来 lib 低于 target 则几乎总是错的:编译器以为你认识某个 API,
产物里却没有对应语法或内置。
在 node_modules/typescript/lib 目录下能看到这摞文件的实物: lib.es5.d.ts、lib.es2015.d.ts……一直到最新版本,外加 lib.dom.d.ts 等 环境描述。原书给的学习建议是:「花几分钟用 Go to Definition 逛逛 lib.es5.d.ts——读懂声明文件的写法,是读懂整个类型生态的钥匙。」21
这次闲逛值得带着三个问题去,收获会完全不同:
- Array<T> 的 T 哪来的? 泛型类型参数(第 16 章主角)在真实声明里
长什么样——
map<U>(callbackfn: (value: T, ...) => U, ...): U[]一行里能看到泛型、函数类型、类型参数推断三个机制同框; - lib.es5.d.ts 和 lib.es2015.d.ts 是什么关系? 版本化的声明文件按年代叠加,lib 选项就是「叠加到哪一年」;
- interface 怎么没有实现? 满目皆是「只有形状的接口 + declare 的变量」——ambient 世界的真实相貌。
答完这三个问题,你再读任何第三方库的 .d.ts(包括 Zod 那种巨型声明), 都不会再觉得是天书:它们全是同一套语法的不同规模的应用。
第二层:DOM 类型。 document、window 的类型住在 lib.dom.d.ts,
用 declare 声明(declare var document: Document)。要它生效,
lib 里加 "dom"(不指定时随 target 默认带上)23。一个有趣的政策:
浏览器功能参差不齐,TypeScript 怎么决定收哪些?——两个主流浏览器支持就收。
这是「全收」和「全不收」之间的平衡;写作当时 DOM 类型已超 28,000 行24。
一个随之而来的现象:DOM 类型经常超前于或落后于你目标浏览器的真实能力—— 类型里有 caniuse 显示你的用户浏览器不支持 API(lib 给了,运行时没有), 或者反过来(你的 polyfill 补上了,类型里没有,需要 declare module 或合并)。 这正是第 6 节「给全局打补丁」的用武之地;理解了 lib 这层供给关系, 「这个浏览器 API 为什么认识/不认识」就不再是玄学。
第三层:库自带的类型。
三层供给的查找顺序值得背下来,排查「这个类型从哪来」时按序查:
① 项目自己的代码(你写的 interface/type)
② node_modules 里库自带的 .d.ts(package.json 的 types 字段指路)
③ @types/ 包(DefinitelyTyped,给不带类型的库)
④ TypeScript 自带的 lib.*.d.ts(JavaScript 与 DOM 环境)
每一层都可能缺、可能错、可能版本不符——「类型对不上」的排查
永远从「这份类型住在第几层」开始。Go to Definition 是最快的层数定位器。 装 npm 包时,很多库在 package.json 里有
"types": "index.d.ts",每个 .js 文件配一张 .d.ts 影子(Zod 就是这样)25。
没有自带类型的库,走 DefinitelyTyped:GitHub 上最大的开源仓库之一,
专门给无类型 JS 库维护高质量定义;装上 @types/库名,TypeScript 立刻认识它。
报错信息自己就会教你:Try npm install --save-dev @types/diff26。
把装完前后的体验对比走一遍:
装前:import Diff from "diff";
❌ Could not find a declaration file for module 'diff'.
Diff.diffChars(...) → 全 any,无补全
pnpm i -D @types/diff 之后:
✅ 导入干净
悬停 Diff.diffChars 的返回值 → Diff.Change[]
自动补全:diffChars、diffLines、diffWords……
@types/* 包的本质,就是别人替这份 JavaScript 写好的声明文件, 按同一套模块解析规则被找到。DefinitelyTyped 是它们的中央仓库, 有完整的测试与 lint 基建——它也是「社区协作维护类型」这个模式 在开源史上数得上的成功案例26。2026 年的现状是:新库大多 TypeScript 原生编写、自带类型,@types 的主要战场越来越集中在 老库和少量不自带类型的主流库上。
skipLibCheck:必须开的逃生门,和它背后的代价¶
几百个 .d.ts 默认都算你项目的一部分——每次编译都被检查。 于是第三方声明文件里的一个错误,能挡住你整个项目的编译。 skipLibCheck: true 让 TypeScript 跳过所有声明文件的检查:必开, 提速又隔离 node_modules 的事故27。
代价同样原书明写:它不区分谁的声明文件——你自己的 .d.ts 写错了, 它也不查。收益(隔离第三方)和代价(自写失明)是同一枚硬币28。
这枚硬币在真实项目里的典型场景:某个 @types 包升级后与你的 tsconfig
不合,几百个错误全从 node_modules 里涌出来——skipLibCheck 一开,清净了。
代价转移到了暗处:同仓库里你自己写的 global.d.ts 里拼错一个属性名,
编译通过,运行时 window.DEBUG 是 undefined。缓解办法是纪律而不是配置:
自写声明文件必须薄(只 declare、只合并),并且要有消费它的代码
(用了 window.DEBUG 的那行代码就是它的测试);声明文件越厚,
skipLibCheck 下的盲区越大。这条纪律和第 6 节「能少就少」是同一条。
skipLibCheck 的普及史也值得知道:TypeScript 项目平均携带几百到上千个 声明文件,其中任意一个的类型错误(版本不匹配、@types 之间的冲突) 都会让 tsc 报出与你代码无关的几百个错。在 skipLibCheck 普及之前, 「升级依赖」的第一道门槛经常是替第三方修类型;开关普及后, 编译回归「检查我自己的代码」。这是 2026 年每个 TypeScript 项目的 默认现实,也是「自写声明文件失明」这个代价被普遍接受的背景27。
类型供给链的总账¶
把「一个名字的 TypeScript 认识与否」背后的完整供给链画出来, 这一章的所有机制都在图上:
你的代码 (.ts) ← 一等公民:被严格检查
│ 引用
▼
同事的 JS (musicPlayer.js) ← 二等公民:同名 .d.ts 翻译,不对照
│
▼
npm 库 (zod / diff) ← 库自带 types 或 @types/* 翻译
│
▼
运行环境 (lib.*.d.ts) ← TypeScript 自带翻译,lib 选项选版本
│
▼
你够不着的世界 (<script> 注入、构建资产) ← declare 家族手写翻译
离你的代码越远,「翻译」的环节越多,信任等级越低—— 这正是「skipLibCheck 必开」「自写声明要薄」「declare 是合同」 三条纪律的共同逻辑:检查力度跟着信任等级走。 你能改的,给你最严格的检查;你改不了的(或不该被拖垮的), 给你最快的编译和最低的噪音。理解了这条信任梯度, 本章的每一条规则都变成同一条规则的推论。
6. 自己写声明文件的正当用例(与两个「别这么做」)¶
用例零(最常见的误解澄清):声明文件不是「类型的家」,是「外来世界的翻译器」。 想清楚「为什么不用 .d.ts 存应用类型」(第 6 节别这么做一)的最好办法, 是把声明文件出现的三个场合排在一起:给外来的 JS 配影子(你不控制实现)、 给全局环境发合同(运行时注入)、给模块/资产补类型(库缺失)—— 三个场合的共同点,是实现都不在这份类型该管的范围里。 一旦实现就在你的代码库里,类型就该和它住一起(.ts 文件), 声明文件退场。这个「类型跟着实现走」的原则,比「少用 .d.ts」的字面规则 更不容易记错。
用例一:给全局补丁打类型(声明合并的正道)。lib.dom.d.ts 里有个 Document 接口;你想给所有 document 加一个自定义属性,建一个无 export 的 global.d.ts,声明一个同名 interface:
同名 interface 合并(第 07/10 章的声明合并在此兑现), 全项目的 document 都有了 foo29。描述「TypeScript 不知道的 JavaScript 全局对象」,这是正规手法。
把这个合并的「生效机制」拆开看一眼,后面再遇到合并不生效就有排查思路:
第 1 步:global.d.ts 没有 export → 它是脚本 → 声明进全局命名空间
第 2 步:interface Document { foo: string } 在全局命名空间里
遇到 lib.dom.d.ts 里的 interface Document
第 3 步:同名 interface 自动合并 → 一个 Document,两份成员
第 4 步:任何地方 document.foo 都有类型
任何一步断了,合并就静默不生效:文件不小心加了 export(第 1 步断)、 名字拼错(第 3 步断)、或者 lib 配置根本没装 dom(第 4 步的对象都不存在)。 「合并没生效」的排查清单,就是这四步的逆序29。
一个真实的调试故事能把这页纸压实:某项目里 global.d.ts 声明了
interface Window { analytics: AnalyticsApi },但 window.analytics
始终报错。按四步逆序查:文件里有 export(第 1 步断——有人顺手加了
export {})→ 删掉后报错消失。这类「补丁为什么不生效」的排查,
在大型项目里平均每周发生一次,而四步清单每次都够用。
这也是为什么原书反复强调 export 的有无:在声明文件的世界里,
export 不只是「导出什么」,更是「这份文件活在哪个世界」的总开关11。
用例二:给非 JavaScript 文件发类型。Webpack 项目里 import 一张 .png, TypeScript 不认识这个模块。declare module 加通配符解决:
从此 import pngUrl from "./example1.png" 拿到 string 类型30。
这道题的完整背景值得说清:报错是 Cannot find module './example1.png' or
its corresponding type declarations——注意后半句「或它的类型声明」:
TypeScript 找模块本来就分两步,先找实现,找不到再找类型声明。
对 .png 这类非 JS 文件,实现永远不可能是「模块」,所以唯一通路就是
类型声明;通配符 declare module 一次性给所有 .png 发了证。
这个「两步查找」机制也解释了第 3 节的报错为什么叫
「找不到声明文件」而不是「找不到模块」——对 .js 文件,实现存在,
缺的一直是影子30。
别这么做一:把应用的类型存进 .d.ts。 有人建 types.d.ts 存公共类型—— 坏主意,因为 skipLibCheck 会跳过对它们的检查:类型写错了没人报。 类型该住在普通 .ts 文件里。经验法则:声明文件能少就少31。
别这么做一(展开): 为什么「类型住 .d.ts」如此普遍地诱人?
因为听起来对——「declaration file 不就是放声明的地方吗」。但拆开看:
skipLibCheck 跳过的是「声明文件」这个类别,不是某种内容;
types.d.ts 里 export type Example = string 写错成 strnig,
tsc 全程沉默,直到某个使用处以诡异的方式暴露。普通 .ts 文件里的类型
享受全套检查、quick fix、重构联动——声明文件里的类型是二等公民。
原书那句「尽量用尽可能少的声明文件」的潜台词是:声明文件是工具,
给外来世界发类型用的;自己的类型体系,请住在一等公民的地盘31。
别这么做二:把常用类型塞进全局。 User 类型到处用,就想 declare global 一把全局可见?原书反对:全局类型制造隐式依赖——不知道类型从哪来, 重构时无从下手;两个模块各自定义 User,名字撞车、含义混淆。 显式 import 让来源清晰、系统可移植、重构容易32。 (这条与第 2 节的 moduleDetection: force 是同一个世界观的两个侧面: 局部性是好东西,全局性是负债。)
7. 练习走查:declare 家族的四个标准动作¶
练习 13-1(给 JS 模块配影子): example.js 导出 myFunc,index.ts 导入报错。
建同名 example.d.ts,写签名 export function myFunc(): string;,
末尾补 export {} 确保它是模块——导入恢复33。
动作要点:同名配对、只写签名、export {} 定身份。
练习 13-1(给 JS 模块配影子)走查: 最朴素的一道,但步骤里有三个易错点。
// index.ts —— 消费方
import {myFunc} from "./example"; // ❌ Could not find a declaration file for module './example'.
myFunc();
三个易错点:
- 文件名必须配对:example.js 的影子必须叫 example.d.ts—— 模块解析按这个同名规则找影子,叫错名字等于没写33;
- 只写签名:影子里的
export function myFunc(): string;以分号结尾, 没有函数体——写了实现就是 ambient 报错(第 15 节)15; - export {} 定身份:影子里加了
export {},它是模块,导出与 example.js 的导出配对;不加,它变脚本,声明落全局—— 导入依然「找得到」,但语义全歪11。
练习 13-2(局部 declare): 文件里的 DEBUG.getState() 报错,DEBUG 是
外部脚本注入的全局,但只在这个文件用。在模块内 declare const DEBUG: {...},
从空对象 {} 逐步补到 {getState: () => {id: string}},报错随类型补全逐步移动、
最终消失——一个「编译器和你结对补类型」的直观演示34。
练习 13-3(给 window 打补丁): 要让 window.DEBUG 合法,需要给内置
Window 接口加属性——正是声明合并:建无 export 的 window.d.ts,
声明 interface Window { DEBUG: {...} },与 lib.dom.d.ts 的 Window 合并35。
等价的替代是在模块里 declare global { interface Window {...} }——
效果相同,但全局类型集中住在专属文件更好找36。
练习 13-3(给 window 打补丁)走查: 报错信息
Property 'DEBUG' does not exist on type 'Window & typeof globalThis' 里
藏着线索——window 的类型是一个接口(Window),而接口是可以合并的
(第 07 章)。所以补法不是 declare const,而是声明一个同名 Window
interface,让它和 lib.dom.d.ts 里的那个合并:
写完后 window.DEBUG.getState() 全通。替代方案是模块内
declare global { interface Window {...} },效果相同;
原书建议全局补丁集中住在专属文件里,好找36。
两种写法的取舍,其实是第 14 章「显式 vs 隐式」主题的微观版: window.d.ts 把补丁集中(好找、好审、好删), declare global 把补丁就近(类型和使用它的代码同文件,改一处看一处)。 项目小时就近方便;补丁多了以后,集中管理的检索优势胜出—— 这和「类型该放哪」的答案是同一个量级的问题,没有绝对, 但默认值应该是「集中」:隐式扩散的代价总是比多走一个文件高36。 注意这道题的「INTERFACE 合并」和 13-2 的「declare const」是两种不同武器: 前者扩的是已有类型,后者造的是新变量——题目报错里说的是 「不存在属性」还是「找不到名字」,决定用哪把。
练习 13-2(续)的完整过程值得细说,因为它示范了 declare 的「渐进精修」:
// 起点:TypeScript 完全不认识 DEBUG
const state = DEBUG.getState(); // ❌ 红线在 DEBUG;悬停 state → any
// 第 1 轮:先让它「存在」
declare const DEBUG: {};
const state = DEBUG.getState(); // ❌ 红线移到 getState:Property does not exist
// 第 2 轮:按报错提示补形状
declare const DEBUG: {
getState: () => { id: string };
};
const state = DEBUG.getState(); // ✅ 全部通过;悬停 state → { id: string }
报错位置从 DEBUG 挪到 getState 再消失——编译器在带着你把类型一寸寸补全。 这套「跟着报错写 declare」的流程,是给任何外来全局写类型的通用流程34。
练习 13-4(给 process.env 打补丁): Node 的环境变量类型住在
@types/node 的 NodeJS.ProcessEnv 接口。要声明 MY_ENV_VAR 必为 string,
同样用合并:建 process.d.ts,declare namespace NodeJS { interface ProcessEnv {
MY_ENV_VAR: string } }——namespace 也能合并(第 10 章的伏笔第三次兑现)37。
13-4 的一个隐藏考点是找到正确的合并目标:process.env 的类型不是 写死在 TypeScript 里的,而是 @types/node 提供的;要改它, 得先知道它住在 NodeJS namespace 的 ProcessEnv 接口里—— 「目标的类型从哪来」这一问,答案永远是 Go to Definition。 报错消失后 MY_ENV_VAR 有了补全与精确类型;原书随即提醒: 报错消失 ≠ 运行时真有这个变量,部署时忘设环境变量,运行时照样 undefined38。
原书对 13-4 补了本章最重要的一句警告:
「报错消失不代表 MY_ENV_VAR 运行时真是 string。这只是一份你和 TypeScript 签的合同——运行时环不遵守,合同照毁。」38
这正是第 05 章「type faith」的第三次回响:declare 也是信仰,不是验证。
四道练习的合成:一张补丁决策表¶
四道练习其实是一张决策表的四行,合起来就是「给外来的全局/内置对象 补类型」的完整流程:
| 场景 | 补法 | 关键动作 |
|---|---|---|
| 外部脚本注入全局(单文件用) | 模块内 declare const |
类型从 {} 逐步补全,跟着报错走 |
| 外部脚本注入全局(项目范围用) | 无 export 的 .d.ts + declare | 文件是脚本,声明自然落全局 |
| 给 window / 内置接口加属性 | 声明合并 Window 接口 | .d.ts 无 export,或用 declare global |
| 给 @types/node 的接口加属性 | declare namespace + interface 合并 | 目标在 NodeJS namespace 里 |
| 给无类型的 npm 包补类型 | declare module "包名" | 想增广别忘 export {} |
| 给 .png 等资产补类型 | declare module "*.png" | 通配符,default 导出为 string |
日常遇到「TypeScript 不认识 X」时,先问:X 是值、接口、模块还是文件? 对上表的哪一行,答案就对应哪一招。四道练习题的价值不在答案本身, 而在把「declare 家族的四种武器」各用了一遍——用一遍,就不会混。
判断(我们的,不是书里的): 「类型影子」这个概念值得单独记住: 声明文件本质是一份编译期合同,合同的双方是「你」和「编译器」, 而永远缺席的第三方是「运行时」。理解了这一点, 第 05 章(type faith)、第 12 章(assertion 无运行时)、第 13 章(declare 是合同) 就是同一句话的三种措辞:TypeScript 的所有「让报错消失」的手段, 没有任何一种能让运行时变得更正确。 这也是 Zod 类运行时校验存在的根本理由—— 类型合同管编译期,运行时合同必须另签(并真的执行)。 如果错,会错在: 随着 TypeScript 的类型推断和生态工具(比如 「类型即 schema」的库)发展,「编译期合同」与「运行时合同」之间的鸿沟 正在被填平——但截至 2026 年,声明文件描述外部世界时,运行时验证依然不可省略。
综合实战:一个项目的环境类型目录长什么样¶
把四道练习的手法合到一个真实项目里,一个典型的 src/types/(或散落的
根级 .d.ts)目录长这样:
src/
window.d.ts ← interface Window 合并:注入的运行时全局(window.DEBUG)
process.d.ts ← declare namespace NodeJS 合并:环境变量合同
assets.d.ts ← declare module "*.png" / "*.css":构建资产的类型
api.d.ts ← declare module "legacy-api":无类型的内部老库
globals.d.ts ←(尽量少)真正跨项目的全局类型,能不用就不用
每一张的职责单一、来源可查:运行时注入的找 window/ProcessEnv, 构建资产找通配符,老库找 declare module。 这个目录的大小本身就是健康指标——文件越多,说明「TypeScript 不知道的外来世界」越多;理想方向是逐版本缩小它(老库重写、 环境变量改由配置模块显式导出、资产导入被框架的类型替代)。 评审 PR 时看到这个目录变大了,值得问一句:「这个全局/这个模块, 有没有更显式的引入方式?」293037
再给一个「从练习到生产」的过渡建议:四道练习的环境都是玩具,
但手法可以直接搬进真实项目。建议的第一个真实练习,是给你自己项目的
「配置模块」写环境类型:很多项目有一个 config.ts,从 process.env
读出一堆配置再导出——给 ProcessEnv 合并上你项目用到的每个变量
(标 string 还是 string | undefined,取决于你想让缺失被编译期抓到),
从此 config.ts 里每个 process.env.X 都有类型与补全,
漏配的环境变量在 CI 的 tsc 阶段就现形。这是投入产出比最高的
声明合并应用,一晚上就能完成并长期受益37。
8. 这一章在全书链条中的位置¶
从这一章起,镜头从「语言」转向「工程」。模块/脚本之分解释了第 07 章 「声明合并为什么是特性」的存在理由(给全局打补丁);declare 的 ambient 语义是第 10 章三个「TypeScript 独有特性」的官方立场声明; type faith 的警告把第 05、06 章的边界讨论收拢成一句「合同要自己守」。 下一章(tsconfig)把本章的配置(moduleDetection、skipLibCheck、lib、target) 收进一份推荐基线——读完两章,你就能把一个 TypeScript 项目配置到 「开箱即严格」。
把两章的关系再压成一句:本章回答「类型和作用域从哪来」,下一章回答 「用哪个编译器人格来检查它们」。同一个项目,strict + force + skipLibCheck 的「严格人格」和全默认的「宽松人格」,检出的错误数量能差出一个数量级—— 而配置就是这个人格的全部来源。这也是为什么原书把 tsconfig 放在语言特性 之后讲:开关的语义,建立在机制的理解之上。原书小结的清单:模块/脚本的判定与 force 开关、声明文件的 两大用途与三个 declare 成员、自带的 lib/DOM 类型、@types 生态、 skipLibCheck 的权衡、以及「应用类型住 .ts、全局类型能免则免」两条纪律39。
9. 作者的判断与证据(分开标)¶
书里有据的:
- 模块/脚本的判定规则与历史成因(比 ES 模块标准更老)——第一人称的设计史叙述3。
- name 撞车事故的完整机制(DOM 的 window.name)——含修复与「全文件皆模块」的推论56。
- 声明文件不与被描述的 JS 对照检查——原书明言 error-prone、不推荐手写10。
- moduleDetection 对 .d.ts 永远 auto——原书明确写出,是容易漏的细节11。
- declare module 的增广/覆盖两面性——含 export {} 的对照实验1819。
- DOM 类型的「两个主流浏览器」收录政策——具体数字(28,000+ 行)在案24。
- skipLibCheck 的「必开 + 自写失明」权衡——原书两边都写了2728。
- 「类型别存 .d.ts」「全局类型别滥用」两条纪律的原书级反对—— 比社区一般讨论的措辞都重,值得按原书引用3132。
- 「类型别存 .d.ts」「全局类型别滥用」两条纪律——原书明确反对+理由3132。
原书没说的(我们补):
- DefinitelyTyped 的规模与治理现状(2026)——补充(来自通用知识): 它仍是社区类型的事实标准,但新库(TypeScript 原生编写)越来越多自带类型, @types 的增量需求在下降。
declare module "*.png"通配符语法与构建工具的对应关系—— 补充: 通配符声明由 TypeScript 的模块解析直接支持,与 Webpack/Vite 的 asset 处理各自独立,类型只是「编译期合同」。- 判断块中的「默认值寿命最长」观察——我们的归纳,非书内说法。
- 「lib.dom.d.ts 的 28,000 行」是写作当时(2025–2026)的数字, 随版本增长——引用时注明时点(补充,来自通用知识)。
pnpm i -D @types/库名的等效命令(npm/yarn)与 monorepo 中 @types 的 提升(pnpm 的 public-hoist)问题——补充(来自通用知识): 安装方式随包管理器而异;pnpm 严格隔离 node_modules 时, @types 偶尔需要手动提升才能被解析到,是 pnpm 用户最常见的类型失踪原因。
判断(我们的,不是书里的): 本章是全书「工程纪律」密度最高的一章, 它的所有规则可以归结为一句:「让依赖显式,让全局最小」。 moduleDetection: force 是让文件身份显式;类型住 .ts、import 代替全局, 是让类型依赖显式;声明文件能少就少、自写要薄,是让检查盲区显式。 这三条都不产生新的能力,只做一件事——把「隐式」换成「显式」, 而「隐式」正是大型 TypeScript 项目腐烂的主要机制(名字撞车、类型失踪、 重构爆炸半径不明)。本章规则值得当作入职培训的第一页。 如果错,会错在: 对小团队和原型,这些纪律的成本高于收益—— 全局类型在五人项目里往往是方便而非混乱,「能少就少」的阈值随团队规模上升。 但作为默认值(尤其 moduleDetection: force),它们在任何规模都不会是负资产。
10. 边界与局限¶
- 手写声明文件的检查真空:skipLibCheck 之下,自写 .d.ts 的唯一「检查」 是使用处是否报错——错误可能潜伏到很深处才显形。给自写声明配测试 (消费它的代码就是测试)是缓解办法。
- declare 合同不担保运行时:13-4 的警告适用于全部 declare—— 环境变量、全局脚本、window 注入,统统「合同自己守」。
- 本章的模块/脚本讨论以编译期作用域为界;运行时的模块解析 (ESM vs CommonJS、打包器的解析规则)是另一层,原书只在 esModuleInterop 等处略提。
- global.d.ts 的声明合并无法给接口加「必填」属性之外的安全保障—— 合并是结构叠加,运行时不存在对应物时照样 type faith。
- 模块/脚本的讨论在 .tsx、vitest 配置、monorepo 边界上各有特例: 本章规则是全集,具体工具链可能有额外的解析层(Vite 的别名、 Jest 的模块映射),排查「找不到模块」时两边都要看。
- skipLibCheck 下「声明文件不能含实现」不会被报告——实现混进 .d.ts 可能静默存在,直到某个工具链环节(或关了 skipLibCheck 的 CI)才炸; 代码评审时对 .d.ts 里的实现代码保持警惕。
- 声明文件的两个「永不」:永不与实现对照检查、永不在运行时存在—— 这两个属性是声明文件一切便利与一切风险的根源,写作前值得各默诵一遍。
- 环境变量合同的双向维护:process.d.ts 声明了 MY_ENV_VAR: string, 部署脚本也得真的设置它;合同只约束编译期,运行时履约靠 CI 的 启动检查或启动时的 schema 校验兜底。
- declare module 增广的精确性依赖原库类型的稳定性:库大版本重构后, 你的增广可能从「叠加」变成「冲突」——升级依赖时,declare module 文件和 @types 包都在复查清单上。
11. 可带走的¶
- 有 import/export = 模块(局部),没有 = 脚本(全局)——脚本里的声明是全局的。
- 新项目第一句配置:
moduleDetection: "force",外加"strict": true。 Cannot redeclare block-scoped variable 'name'= 文件被当脚本、撞了 DOM 全局; 修复:export {}或 force。- 声明文件 = 同名 .d.ts 的「类型影子」,只写类型不写实现;不和真实 JS 对照检查。
- declare = 无实现声明;位置决定作用域(.d.ts 无 export = 全局;模块内 = 局部);
declare global从模块向全局开口,块内不再写 declare。 declare module在模块态是增广、脚本态是覆盖——export {} 一行定生死。- 每个项目几百个声明文件:lib..d.ts(JavaScript 各版本)、lib.dom.d.ts(浏览器)、 库自带 types、@types/(DefinitelyTyped)。
skipLibCheck: true必开——代价是自己的 .d.ts 也失明,所以应用类型住 .ts 文件。- 声明合并是给全局/内置接口打补丁的正规手法(window.DEBUG、process.env 都靠它)。
declare module "*.png"给非 JS 资源发类型;通配符不能配相对名。- 全局类型能免则免——隐式依赖难追溯、名字会撞;显式 import 是纪律。
- declare 是合同不是验证——「报错消失 ≠ 运行时正确」。
- 「TypeScript 不认识 X」先分类:值/接口/模块/文件,对应 declare 家族的四种武器。
- skipLibCheck 下的自写声明要薄、要有消费代码当测试。
- 找模块永远问两句:实现存在吗?类型声明存在吗?——两步查找。
- declare 的渐进精修:先让它存在(空对象),再跟着报错补形状。
- 「合并没生效」排查:文件是脚本吗?名字对吗?lib 装了吗?——四步逆序。
- 类型跟着实现走;声明文件只翻译外来的世界。
- 声明文件的两个「永不」:不与实现对照、不在运行时存在。
- 第一个声明合并实战:给自己的 config.ts 用到的环境变量签合同。
把全章折成一条「全局污染事故」的排查剧本,遇到事故按幕走:
第一幕:症状。某文件突然多出几十条「Cannot redeclare」或
明明没 import 却「认识」一个陌生的全局名字。
第二幕:定位。是哪份文件没有 import/export(成了脚本)?
它的哪个声明漏进了全局?——tsc 报错里重复声明的列表
就是嫌疑人名单(第 1 节)。
第三幕:止血。给嫌疑人文件加 export {} 一行,身份立即变回模块
(第 2 节的 moduleDetection 是事前疫苗,export {} 是事后急救)。
第四幕:根治。tsconfig 里 moduleDetection: "force"——
让「脚本」成为需要显式 opt-in 的稀有物种,事故从源头消失。
图说:四幕对应「症状 → 定位 → 止血 → 根治」,
每一步的工具本章都备好了,这条剧本是把它们串起来的线。
如果是「全局类型突然不生效」而不是「重复声明」,把剧本倒过来走:
先查配置归属(第 15 章的项目引用),再查文件身份——
消失的声明和冒出来的全局,是同一枚硬币的两面。
这条剧本刻意用「事故」而非「概念」来组织:概念考完就忘,事故遇一次记一年。
第 14、15 两章合起来,就是「让事故尽量发生在编译期」这个总目标的两套工具:
本章管作用域,下一章管严格度。两章都不产生新语法,只决定已有语法的检查力度——
这也是它们排在「设计类型」两章之前的原因:先把地基的松紧调对,再往上盖类型的楼。
反过来也成立:第 16、17 章造的每一个复杂类型,最终都要靠这两章的开关
保住检查的有效性——楼盖得越高,地基的验收越不能省。
12. 原文地图¶
| 主题 | 原书位置 | 原文位置 |
|---|---|---|
| 模块 = 局部作用域 | Understanding Modules and Scripts | text/29-fm-understanding-modules-and-scripts.txt:14(搜「isolated piece of code」) |
| 脚本 = 全局作用域 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:33(搜「Scripts Have Global Scope」) |
| 猜测行为的历史成因 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:52(搜「older than the standardized」) |
| type=module 与环境决定 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:58(搜「opt in to using modules」) |
| name 撞车事故 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:71(搜「Cannot redeclare」) |
| export {} 修复 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:79(搜「export {}」) |
| moduleDetection 三选项 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:93(搜「auto, force, and legacy」) |
| 声明文件的两大用途 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:111(搜「two main purposes」) |
| 描述 JS(musicPlayer) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:135(搜「playTrack(track」) |
| 不与 JS 对照检查 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:174(搜「not checked against」) |
| .d.ts 永远 auto | 同上 | text/29-fm-understanding-modules-and-scripts.txt:182(搜「always set to auto」) |
| ambient 与实现报错 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:212(搜「ambient contexts」) |
| declare 家族 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:271(搜「declare const MY_CONSTANT」) |
| declare global 与内部不再 declare | 同上 | text/29-fm-understanding-modules-and-scripts.txt:291(搜「already ambient」) |
| declare module 基本形 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:315(搜「duration-utils」) |
| 增广(带 export {}) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:345(搜「Adding an export」) |
| 覆盖(删 export {}) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:366(搜「completely overridden」) |
| lib.es5.d.ts 的 map | 同上 | text/29-fm-understanding-modules-and-scripts.txt:396(搜「callbackfn」) |
| lib 选项与继承 target | 同上 | text/29-fm-understanding-modules-and-scripts.txt:426(搜「inherits from the target」) |
| DOM 类型与两浏览器政策 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:459(搜「two major browsers」) |
| 库自带 types(zod) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:467(搜「types key」) |
| DefinitelyTyped 与 @types/diff | 同上 | text/29-fm-understanding-modules-and-scripts.txt:507(搜「npm install --save-dev」) |
| skipLibCheck 必开 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:533(搜「must-have」) |
| 自写声明失明 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:537(搜「won’t catch it」) |
| Document 声明合并 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:555(搜「interface Document」) |
| declare module "*.png" | 同上 | text/29-fm-understanding-modules-and-scripts.txt:586(搜「*.png」) |
| 「类型别存 .d.ts」 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:614(搜「as few declaration files as possible」) |
| 「全局类型别滥用」 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:622(搜「implicit dependencies」) |
| 练习 13-2(局部 declare) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:689(搜「declare const DEBUG」) |
| 练习 13-3(window 补丁) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:734(搜「interface Window」) |
| 练习 13-4(process.env) | 同上 | text/29-fm-understanding-modules-and-scripts.txt:795(搜「declare namespace NodeJS」) |
| Window 合并的四步排查 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:182(搜「moduleDetection is always set to auto」) |
| 「合同自己守」警告 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:803(搜「merely a contract」) |
| 「三步查找」与 .png 报错 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:579(搜「or its corresponding type declarations」) |
| 「模块身份的历史默认」 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:63(搜「99 percent」) |
| 「@types 装完前后」体验 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:517(搜「Diff.Change[]」) |
| declare 的渐进精修流程 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:692(搜「red squiggly line under getState」) |
| 声明合并的生效机制 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:559(搜「being treated as a script」) |
| 13-4 的「合同」警告 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:803(搜「respected in your runtime environment」) |
| 本章小结 | 同上 | text/29-fm-understanding-modules-and-scripts.txt:809(搜「local scope while scripts」) |
最后把本章收进一句可带走的总纲:「模块显式化、声明最小化、翻译薄而可审」。 前四个字属于第 2 节(force 开关与 export 纪律),中间四个字属于第 6 节 (类型住 .ts、声明文件只留外来翻译),后六个字属于第 5 节 (skipLibCheck 下的自写纪律)。三句话各自独立,合起来就是 一个 TypeScript 项目与「类型供给的灰色地带」相处的完整章程。
-
出处:「Understanding Modules and Scripts」第 12–31 段(
text/29-fm-understanding-modules-and-scripts.txt:14,搜「isolated piece of code」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 33–46 段(
text/29-fm-understanding-modules-and-scripts.txt:33,搜「Scripts Have Global Scope」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 50–56 段(
text/29-fm-understanding-modules-and-scripts.txt:52,搜「older than the standardized」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 56–61 段(
text/29-fm-understanding-modules-and-scripts.txt:58,搜「opt in to using modules」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 63–85 段(
text/29-fm-understanding-modules-and-scripts.txt:71,搜「Cannot redeclare」);window.name 的说明见第 85 段(搜「targets for hyperlinks」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 75–87 段(
text/29-fm-understanding-modules-and-scripts.txt:79,搜「export {}」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 91–107 段(
text/29-fm-understanding-modules-and-scripts.txt:93,搜「auto, force, and legacy」)。 ↩ -
出处:「Understanding Modules and Scripts」第 109–155 段(
text/29-fm-understanding-modules-and-scripts.txt:135,搜「playTrack(track」)。 ↩ -
出处:「Understanding Modules and Scripts」第 157–172 段(
text/29-fm-understanding-modules-and-scripts.txt:172,搜「Track, playTrack」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 174–176 段(
text/29-fm-understanding-modules-and-scripts.txt:174,搜「not checked against」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 178–194 段(
text/29-fm-understanding-modules-and-scripts.txt:182,搜「always set to auto」)。 ↩↩↩↩ -
出处:「Understanding Modules and Scripts」第 218–242 段(
text/29-fm-understanding-modules-and-scripts.txt:237,搜「declare const ALBUM_API」)。 ↩ -
出处:「Understanding Modules and Scripts」第 244–265 段(
text/29-fm-understanding-modules-and-scripts.txt:265,搜「scoped to the musicUtils.ts module」)。 ↩ -
出处:「Understanding Modules and Scripts」第 267–276 段(
text/29-fm-understanding-modules-and-scripts.txt:271,搜「declare const MY_CONSTANT」)。 ↩ -
出处:「Understanding Modules and Scripts」第 196–216 段(
text/29-fm-understanding-modules-and-scripts.txt:212,搜「ambient contexts」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 278–304 段(
text/29-fm-understanding-modules-and-scripts.txt:291,搜「already ambient」)。 ↩ -
出处:「Understanding Modules and Scripts」第 306–328 段(
text/29-fm-understanding-modules-and-scripts.txt:315,搜「duration-utils」);「不对照、要手动同步」见第 328 段(搜「keep them up to date」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 330–354 段(
text/29-fm-understanding-modules-and-scripts.txt:345,搜「Adding an export」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 356–373 段(
text/29-fm-understanding-modules-and-scripts.txt:366,搜「completely overridden」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 352、371 段(
text/29-fm-understanding-modules-and-scripts.txt:371,搜「changing express.d.ts to express.ts」)。 ↩ -
出处:「Understanding Modules and Scripts」第 379–413 段(
text/29-fm-understanding-modules-and-scripts.txt:396,搜「callbackfn」)、第 409 段(搜「lib.es2021.string.d.ts」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 415–426 段(
text/29-fm-understanding-modules-and-scripts.txt:426,搜「inherits from the target」)。 ↩ -
出处:「Understanding Modules and Scripts」第 428–455 段(
text/29-fm-understanding-modules-and-scripts.txt:436,搜「declare var document」)。 ↩ -
出处:「Understanding Modules and Scripts」第 457–459 段(
text/29-fm-understanding-modules-and-scripts.txt:459,搜「two major browsers」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 461–485 段(
text/29-fm-understanding-modules-and-scripts.txt:467,搜「types key」)。 ↩ -
出处:「Understanding Modules and Scripts」第 487–519 段(
text/29-fm-understanding-modules-and-scripts.txt:507,搜「npm install --save-dev」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 521–533 段(
text/29-fm-understanding-modules-and-scripts.txt:533,搜「must-have」)。 ↩↩↩↩ -
出处:「Understanding Modules and Scripts」第 535–539 段(
text/29-fm-understanding-modules-and-scripts.txt:537,搜「won’t catch it」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 541–567 段(
text/29-fm-understanding-modules-and-scripts.txt:555,搜「interface Document」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 569–592 段(
text/29-fm-understanding-modules-and-scripts.txt:586,搜「*.png」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 594–614 段(
text/29-fm-understanding-modules-and-scripts.txt:614,搜「as few declaration files as possible」)。 ↩↩↩↩ -
出处:「Understanding Modules and Scripts」第 616–626 段(
text/29-fm-understanding-modules-and-scripts.txt:622,搜「implicit dependencies」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 628–662 段(
text/29-fm-understanding-modules-and-scripts.txt:658,搜「myFunc(): string」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 664–704 段(
text/29-fm-understanding-modules-and-scripts.txt:689,搜「declare const DEBUG」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 706–742 段(
text/29-fm-understanding-modules-and-scripts.txt:734,搜「interface Window」)。 ↩ -
出处:「Understanding Modules and Scripts」第 744–763 段(
text/29-fm-understanding-modules-and-scripts.txt:753,搜「declare global」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 765–803 段(
text/29-fm-understanding-modules-and-scripts.txt:795,搜「declare namespace NodeJS」)。 ↩↩↩ -
出处:「Understanding Modules and Scripts」第 803 段(
text/29-fm-understanding-modules-and-scripts.txt:803,搜「merely a contract」)。 ↩↩ -
出处:「Understanding Modules and Scripts」第 805–815 段(
text/29-fm-understanding-modules-and-scripts.txt:809,搜「local scope while scripts」)。 ↩ -
出处:「Understanding Modules and Scripts」第 63 段(
text/29-fm-understanding-modules-and-scripts.txt:63,搜「99 percent」)。原文:「these days, 99 percent of the code you’ll be writing will be in modules」。 ↩↩