编程语言研究书库

模块、脚本与声明文件 — 作用域的两个世界与 .d.ts 的来历

模块、脚本与声明文件 — 作用域的两个世界与 .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 年的现实5。

先把「谁说了算」这张图钉牢:

  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,写一行:

// utils.ts
const name = "Alice"; // ❌ Cannot redeclare block-scoped variable 'name'.

你没声明过 name,凭什么说重复?——因为 utils.ts 没有 import/export, 被当成了脚本,name 落进了全局作用域;而全局作用域里已经有一个 DOM 声明的 name 变量(window.name,给超链接和表单设目标的那个)6。

修复方法奇特得可爱:在文件末尾加一个空的 export:

const name = "Alice";

export {}; // 仅此一行,错误消失

export {} 让文件变成模块,name 回到模块局部作用域,与全局的 window.name 井水不犯河水7。这场事故是「所有文件都该是模块」的最好论据—— TypeScript 也给了正式开关:

// tsconfig.json
{
  "compilerOptions": {
    "moduleDetection": "force"
  }
}

moduleDetection 有三个值:auto(默认,靠猜)、force(全部当模块)、 legacy(只用于兼容老版本,可无视)。force 之后,所有文件都必须 import/export 才能互通,「意外的全局变量」这一整类事故消失8。

迁移到 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」的判断5, 就是 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 获得了完整的检查与补全9。 声明文件里也能导出接口和类型,供其它文件复用10。

但原书立刻敲了警钟:声明文件不会被拿来和它描述的 JavaScript 对照检查。 你在 .d.ts 里把 playTrack 误写成 playTRACK,TypeScript 一声不吭—— 两张皮,各说各话11。所以「手写声明文件描述自己的 JS」是易错且通常 不推荐的做法;它是给「无法修改的 JavaScript」准备的工具,不是日常路径。

把这张影子的「身份」再想清楚一层:musicPlayer.d.ts 因为有 export, 自己是模块——它的导出与 musicPlayer.js 的导出按「同名配对」关联, 靠的是模块解析规则,不是任何显式绑定。这也解释了为什么声明文件里 可以导出 interface 和 type:影子文件是模块,模块导出什么都行10。 如果影子文件没有 export,它就变成脚本,声明落全局——下一节的主角。

4. 全局作用域与 declare 全家桶

声明文件的第二用途:给全局加类型。规则和第 2 节一致—— .d.ts 文件同样按有没有 export 分模块/脚本;没有 export 的 .d.ts 是脚本, 里面的声明直接落进全局。注意 moduleDetection: force 管不到这里: 对 .d.ts 文件,moduleDetection 永远是 auto12。

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,还有补全13。

把这段声明的「形状」记一下,后面所有 declare 都是它的变体:

  declare + (const | var | let | function | class | enum) + 名字 + 类型
  语义:在类型世界里,「这里存在一个叫 X 的东西,形状如下」——
        不分配内存、不产生代码、编译后整个消失。

它的价值恰恰在于「消失」:ALBUM_API 的真实实现是别人那个 <script> 提供的, TypeScript 只负责在编译期相信你说的形状;如果形状说错了, 后果就是第 5 章那句 type faith——所以写 declare 时, 悬停参考的应该是真实脚本的真实 API,而不是想象中的 API。

declare 的位置决定作用域: 把这段挪进一个有 export 的 .ts 模块文件里,ALBUM_API 就只在该模块内可见 ——「我知道这个全局存在,但只有我这个文件用」的局部声明14。

declare 的家族不止 const:declare var、declare let、declare function, 甚至 enum、class、class 字段都行——共同点是只声明、不实现15。 TypeScript 管这类「没有实现的世界」叫 ambient(环境):声明文件里的一切 默认是 ambient,所以在 .d.ts 里写实现会直接报错 (An implementation cannot be declared in ambient contexts)16。

原书在这里埋了一个细节,值得单独点名:这个报错只在 skipLibCheck: false 时 才出现。第 5 节会讲到,skipLibCheck 打开后所有声明文件(包括你刚写错的这个) 都不再被检查——于是「声明文件不能含实现」这条规则,在你自己的 .d.ts 上 可能根本不会被报告。这不是规则的例外,而是检查范围的副作用: skipLibCheck 的「不查」是文件级的一刀切,不区分文件是谁写的。 把这两个知识点连起来,你就理解了为什么第 6 节要说「自写声明要薄」—— 厚声明文件在 skipLibCheck 下是零护栏的1617。

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 进全局18。这个「报错教你规则」的循环, 是 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" 通过。 同样只声明不实现,也不会和真实模块对照——要手工保持同步19。

完整的消费体验走一遍:没有这张声明文件时, import {formatDuration} from "duration-utils" 报 「找不到声明文件」,并且就算你无视红线,formatDuration 也是 any 待遇—— 参数数量、返回值,什么都管不了。声明文件落地后,同一个导入获得 完整签名:formatDuration(309) 有补全、传错类型有红线、 悬停显示 (seconds: number) => string。 注意一个心智转变:你写的这张声明,读者不只是编译器, 还有三个月后接手的人——把每个参数名、返回值类型写清楚, 就是这份文件里唯一存在的「实现」。

declare module 还有一个进阶形态值得知道:模块名支持通配符, 第 6 节给 .png 发类型就是用它;而当你要描述「一个包里的某个子路径」时, 也可以精确到路径(declare module "lodash/debounce")。 原书没展开这些变体,但机制相同——declare module 的本质是 给模块解析加一个「类型先到」的旁路,旁路的一切便利与风险 (不对照、要手动同步)都不变19。

增广还是覆盖:一行 export {} 的天平

declare module 有个微妙的两面性,取决于文件是模块还是脚本:

// express.d.ts —— 文件底部有 export {}
declare module "express" {
  export interface MyType { hello: string; }
}
export {};

模块态(有 export {}):declare module 是「增广」——往已有的 express 模块 上追加新类型,原有导出原封不动20。

// express.d.ts —— 删掉了 export {}
declare module "express" {
  export interface MyType { hello: string; }
}

脚本态(无 export):declare module 是「覆盖」——express 模块的类型 被你整个替换,现在它只剩 MyType;import {express} from "express" 反而报错21。

一行之差,语义天翻地覆。覆盖模式偶尔有用(第三方库类型全错、整体重写), 但多数时候你想要的是增广。记住这个天平,诊断「我给库补的类型怎么把整个库 搞没了」时它就是答案。(同一行为在普通 .ts 文件里也成立—— moduleDetection: auto 时,把 express.d.ts 改名 express.ts 效果相同22。)

增广还是覆盖:一个可以当场做的判定实验

这个两面性值得亲手验证一次,因为它太反直觉:

  实验设置:项目里装了 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 {}(或多了,取决于你想要哪一边)。2021

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.ts23。tsconfig 的 lib 选项决定把哪些版本装进项目: "lib": ["es2022"] 拿到 ES2022 及之前的一切;不指定时 lib 继承 target24。 注意这两件事是分离的: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——读懂声明文件的写法,是读懂整个类型生态的钥匙。」23

这次闲逛值得带着三个问题去,收获会完全不同:

  1. Array<T> 的 T 哪来的? 泛型类型参数(第 16 章主角)在真实声明里 长什么样——map<U>(callbackfn: (value: T, ...) => U, ...): U[] 一行里能看到泛型、函数类型、类型参数推断三个机制同框;
  2. lib.es5.d.ts 和 lib.es2015.d.ts 是什么关系? 版本化的声明文件按年代叠加,lib 选项就是「叠加到哪一年」;
  3. interface 怎么没有实现? 满目皆是「只有形状的接口 + declare 的变量」——ambient 世界的真实相貌。

答完这三个问题,你再读任何第三方库的 .d.ts(包括 Zod 那种巨型声明), 都不会再觉得是天书:它们全是同一套语法的不同规模的应用。

第二层:DOM 类型。 document、window 的类型住在 lib.dom.d.ts, 用 declare 声明(declare var document: Document)。要它生效, lib 里加 "dom"(不指定时随 target 默认带上)25。一个有趣的政策: 浏览器功能参差不齐,TypeScript 怎么决定收哪些?——两个主流浏览器支持就收。 这是「全收」和「全不收」之间的平衡;写作当时 DOM 类型已超 28,000 行26。

一个随之而来的现象: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 就是这样)27。 没有自带类型的库,走 DefinitelyTyped:GitHub 上最大的开源仓库之一, 专门给无类型 JS 库维护高质量定义;装上 @types/库名,TypeScript 立刻认识它。 报错信息自己就会教你:Try npm install --save-dev @types/diff28。

把装完前后的体验对比走一遍:

  装前: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 基建——它也是「社区协作维护类型」这个模式 在开源史上数得上的成功案例28。2026 年的现状是:新库大多 TypeScript 原生编写、自带类型,@types 的主要战场越来越集中在 老库和少量不自带类型的主流库上。

skipLibCheck:必须开的逃生门,和它背后的代价

几百个 .d.ts 默认都算你项目的一部分——每次编译都被检查。 于是第三方声明文件里的一个错误,能挡住你整个项目的编译。 skipLibCheck: true 让 TypeScript 跳过所有声明文件的检查:必开, 提速又隔离 node_modules 的事故17。

代价同样原书明写:它不区分谁的声明文件——你自己的 .d.ts 写错了, 它也不查。收益(隔离第三方)和代价(自写失明)是同一枚硬币29。

这枚硬币在真实项目里的典型场景:某个 @types 包升级后与你的 tsconfig 不合,几百个错误全从 node_modules 里涌出来——skipLibCheck 一开,清净了。 代价转移到了暗处:同仓库里你自己写的 global.d.ts 里拼错一个属性名, 编译通过,运行时 window.DEBUG 是 undefined。缓解办法是纪律而不是配置: 自写声明文件必须薄(只 declare、只合并),并且要有消费它的代码 (用了 window.DEBUG 的那行代码就是它的测试);声明文件越厚, skipLibCheck 下的盲区越大。这条纪律和第 6 节「能少就少」是同一条。

skipLibCheck 的普及史也值得知道:TypeScript 项目平均携带几百到上千个 声明文件,其中任意一个的类型错误(版本不匹配、@types 之间的冲突) 都会让 tsc 报出与你代码无关的几百个错。在 skipLibCheck 普及之前, 「升级依赖」的第一道门槛经常是替第三方修类型;开关普及后, 编译回归「检查我自己的代码」。这是 2026 年每个 TypeScript 项目的 默认现实,也是「自写声明文件失明」这个代价被普遍接受的背景17。

类型供给链的总账

把「一个名字的 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:

// global.d.ts —— 脚本,全局
interface Document {
  foo: string;
}

同名 interface 合并(第 07/10 章的声明合并在此兑现), 全项目的 document 都有了 foo30。描述「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 步的对象都不存在)。 「合并没生效」的排查清单,就是这四步的逆序30。

一个真实的调试故事能把这页纸压实:某项目里 global.d.ts 声明了 interface Window { analytics: AnalyticsApi },但 window.analytics 始终报错。按四步逆序查:文件里有 export(第 1 步断——有人顺手加了 export {})→ 删掉后报错消失。这类「补丁为什么不生效」的排查, 在大型项目里平均每周发生一次,而四步清单每次都够用。 这也是为什么原书反复强调 export 的有无:在声明文件的世界里, export 不只是「导出什么」,更是「这份文件活在哪个世界」的总开关12。

用例二:给非 JavaScript 文件发类型。Webpack 项目里 import 一张 .png, TypeScript 不认识这个模块。declare module 加通配符解决:

// images.d.ts
declare module "*.png" {
  const png: string;
  export default png;
}

从此 import pngUrl from "./example1.png" 拿到 string 类型31。

这道题的完整背景值得说清:报错是 Cannot find module './example1.png' or its corresponding type declarations——注意后半句「或它的类型声明」: TypeScript 找模块本来就分两步,先找实现,找不到再找类型声明。 对 .png 这类非 JS 文件,实现永远不可能是「模块」,所以唯一通路就是 类型声明;通配符 declare module 一次性给所有 .png 发了证。 这个「两步查找」机制也解释了第 3 节的报错为什么叫 「找不到声明文件」而不是「找不到模块」——对 .js 文件,实现存在, 缺的一直是影子31。

别这么做一:把应用的类型存进 .d.ts。 有人建 types.d.ts 存公共类型—— 坏主意,因为 skipLibCheck 会跳过对它们的检查:类型写错了没人报。 类型该住在普通 .ts 文件里。经验法则:声明文件能少就少32。

别这么做一(展开): 为什么「类型住 .d.ts」如此普遍地诱人? 因为听起来对——「declaration file 不就是放声明的地方吗」。但拆开看: skipLibCheck 跳过的是「声明文件」这个类别,不是某种内容; types.d.ts 里 export type Example = string 写错成 strnig, tsc 全程沉默,直到某个使用处以诡异的方式暴露。普通 .ts 文件里的类型 享受全套检查、quick fix、重构联动——声明文件里的类型是二等公民。 原书那句「尽量用尽可能少的声明文件」的潜台词是:声明文件是工具, 给外来世界发类型用的;自己的类型体系,请住在一等公民的地盘32。

别这么做二:把常用类型塞进全局。 User 类型到处用,就想 declare global 一把全局可见?原书反对:全局类型制造隐式依赖——不知道类型从哪来, 重构时无从下手;两个模块各自定义 User,名字撞车、含义混淆。 显式 import 让来源清晰、系统可移植、重构容易33。 (这条与第 2 节的 moduleDetection: force 是同一个世界观的两个侧面: 局部性是好东西,全局性是负债。)

7. 练习走查:declare 家族的四个标准动作

练习 13-1(给 JS 模块配影子): example.js 导出 myFunc,index.ts 导入报错。 建同名 example.d.ts,写签名 export function myFunc(): string;, 末尾补 export {} 确保它是模块——导入恢复34。 动作要点:同名配对、只写签名、export {} 定身份。

练习 13-1(给 JS 模块配影子)走查: 最朴素的一道,但步骤里有三个易错点。

// example.js —— 被描述方
export const myFunc = () => {
  return "Hello World!";
};
// index.ts —— 消费方
import {myFunc} from "./example"; // ❌ Could not find a declaration file for module './example'.
myFunc();

三个易错点:

  1. 文件名必须配对:example.js 的影子必须叫 example.d.ts—— 模块解析按这个同名规则找影子,叫错名字等于没写34;
  2. 只写签名:影子里的 export function myFunc(): string; 以分号结尾, 没有函数体——写了实现就是 ambient 报错(第 15 节)16;
  3. export {} 定身份:影子里加了 export {},它是模块,导出与 example.js 的导出配对;不加,它变脚本,声明落全局—— 导入依然「找得到」,但语义全歪12。

练习 13-2(局部 declare): 文件里的 DEBUG.getState() 报错,DEBUG 是 外部脚本注入的全局,但只在这个文件用。在模块内 declare const DEBUG: {...}, 从空对象 {} 逐步补到 {getState: () => {id: string}},报错随类型补全逐步移动、 最终消失——一个「编译器和你结对补类型」的直观演示35。

练习 13-3(给 window 打补丁): 要让 window.DEBUG 合法,需要给内置 Window 接口加属性——正是声明合并:建无 export 的 window.d.ts, 声明 interface Window { DEBUG: {...} },与 lib.dom.d.ts 的 Window 合并36。 等价的替代是在模块里 declare global { interface Window {...} }—— 效果相同,但全局类型集中住在专属文件更好找37。

练习 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.d.ts —— 关键:没有 export,是脚本
interface Window {
  DEBUG: {
    getState: () => { id: string };
  };
}

写完后 window.DEBUG.getState() 全通。替代方案是模块内 declare global { interface Window {...} },效果相同; 原书建议全局补丁集中住在专属文件里,好找37。

两种写法的取舍,其实是第 14 章「显式 vs 隐式」主题的微观版: window.d.ts 把补丁集中(好找、好审、好删), declare global 把补丁就近(类型和使用它的代码同文件,改一处看一处)。 项目小时就近方便;补丁多了以后,集中管理的检索优势胜出—— 这和「类型该放哪」的答案是同一个量级的问题,没有绝对, 但默认值应该是「集中」:隐式扩散的代价总是比多走一个文件高37。 注意这道题的「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」的流程,是给任何外来全局写类型的通用流程35。

练习 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 章的伏笔第三次兑现)38。

13-4 的一个隐藏考点是找到正确的合并目标:process.env 的类型不是 写死在 TypeScript 里的,而是 @types/node 提供的;要改它, 得先知道它住在 NodeJS namespace 的 ProcessEnv 接口里—— 「目标的类型从哪来」这一问,答案永远是 Go to Definition。 报错消失后 MY_ENV_VAR 有了补全与精确类型;原书随即提醒: 报错消失 ≠ 运行时真有这个变量,部署时忘设环境变量,运行时照样 undefined39。

原书对 13-4 补了本章最重要的一句警告:

「报错消失不代表 MY_ENV_VAR 运行时真是 string。这只是一份你和 TypeScript 签的合同——运行时环不遵守,合同照毁。」39

这正是第 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 时看到这个目录变大了,值得问一句:「这个全局/这个模块, 有没有更显式的引入方式?」303138

再给一个「从练习到生产」的过渡建议:四道练习的环境都是玩具, 但手法可以直接搬进真实项目。建议的第一个真实练习,是给你自己项目的 「配置模块」写环境类型:很多项目有一个 config.ts,从 process.env 读出一堆配置再导出——给 ProcessEnv 合并上你项目用到的每个变量 (标 string 还是 string | undefined,取决于你想让缺失被编译期抓到), 从此 config.ts 里每个 process.env.X 都有类型与补全, 漏配的环境变量在 CI 的 tsc 阶段就现形。这是投入产出比最高的 声明合并应用,一晚上就能完成并长期受益38。

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、全局类型能免则免」两条纪律40。

9. 作者的判断与证据(分开标)

书里有据的:

  • 模块/脚本的判定规则与历史成因(比 ES 模块标准更老)——第一人称的设计史叙述3。
  • name 撞车事故的完整机制(DOM 的 window.name)——含修复与「全文件皆模块」的推论67。
  • 声明文件不与被描述的 JS 对照检查——原书明言 error-prone、不推荐手写11。
  • moduleDetection 对 .d.ts 永远 auto——原书明确写出,是容易漏的细节12。
  • declare module 的增广/覆盖两面性——含 export {} 的对照实验2021。
  • DOM 类型的「两个主流浏览器」收录政策——具体数字(28,000+ 行)在案26。
  • skipLibCheck 的「必开 + 自写失明」权衡——原书两边都写了1729。
  • 「类型别存 .d.ts」「全局类型别滥用」两条纪律的原书级反对—— 比社区一般讨论的措辞都重,值得按原书引用3233。
  • 「类型别存 .d.ts」「全局类型别滥用」两条纪律——原书明确反对+理由3233。

原书没说的(我们补):

  • 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. 可带走的

  1. 有 import/export = 模块(局部),没有 = 脚本(全局)——脚本里的声明是全局的。
  2. 新项目第一句配置:moduleDetection: "force",外加 "strict": true。
  3. Cannot redeclare block-scoped variable 'name' = 文件被当脚本、撞了 DOM 全局; 修复:export {} 或 force。
  4. 声明文件 = 同名 .d.ts 的「类型影子」,只写类型不写实现;不和真实 JS 对照检查。
  5. declare = 无实现声明;位置决定作用域(.d.ts 无 export = 全局;模块内 = 局部); declare global 从模块向全局开口,块内不再写 declare。
  6. declare module 在模块态是增广、脚本态是覆盖——export {} 一行定生死。
  7. 每个项目几百个声明文件:lib..d.ts(JavaScript 各版本)、lib.dom.d.ts(浏览器)、 库自带 types、@types/(DefinitelyTyped)。
  8. skipLibCheck: true 必开——代价是自己的 .d.ts 也失明,所以应用类型住 .ts 文件。
  9. 声明合并是给全局/内置接口打补丁的正规手法(window.DEBUG、process.env 都靠它)。
  10. declare module "*.png" 给非 JS 资源发类型;通配符不能配相对名。
  11. 全局类型能免则免——隐式依赖难追溯、名字会撞;显式 import 是纪律。
  12. declare 是合同不是验证——「报错消失 ≠ 运行时正确」。
  13. 「TypeScript 不认识 X」先分类:值/接口/模块/文件,对应 declare 家族的四种武器。
  14. skipLibCheck 下的自写声明要薄、要有消费代码当测试。
  15. 找模块永远问两句:实现存在吗?类型声明存在吗?——两步查找。
  16. declare 的渐进精修:先让它存在(空对象),再跟着报错补形状。
  17. 「合并没生效」排查:文件是脚本吗?名字对吗?lib 装了吗?——四步逆序。
  18. 类型跟着实现走;声明文件只翻译外来的世界。
  19. 声明文件的两个「永不」:不与实现对照、不在运行时存在。
  20. 第一个声明合并实战:给自己的 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 Scriptstext/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 项目与「类型供给的灰色地带」相处的完整章程。

Footnotes

  1. 出处:「Understanding Modules and Scripts」第 12–31 段(text/29-fm-understanding-modules-and-scripts.txt:14,搜「isolated piece of code」)。 ↩ ↩2

  2. 出处:「Understanding Modules and Scripts」第 33–46 段(text/29-fm-understanding-modules-and-scripts.txt:33,搜「Scripts Have Global Scope」)。 ↩ ↩2

  3. 出处:「Understanding Modules and Scripts」第 50–56 段(text/29-fm-understanding-modules-and-scripts.txt:52,搜「older than the standardized」)。 ↩ ↩2

  4. 出处:「Understanding Modules and Scripts」第 56–61 段(text/29-fm-understanding-modules-and-scripts.txt:58,搜「opt in to using modules」)。 ↩ ↩2

  5. 出处:「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」。 ↩ ↩2

  6. 出处:「Understanding Modules and Scripts」第 63–85 段(text/29-fm-understanding-modules-and-scripts.txt:71,搜「Cannot redeclare」);window.name 的说明见第 85 段(搜「targets for hyperlinks」)。 ↩ ↩2

  7. 出处:「Understanding Modules and Scripts」第 75–87 段(text/29-fm-understanding-modules-and-scripts.txt:79,搜「export {}」)。 ↩ ↩2

  8. 出处:「Understanding Modules and Scripts」第 91–107 段(text/29-fm-understanding-modules-and-scripts.txt:93,搜「auto, force, and legacy」)。 ↩

  9. 出处:「Understanding Modules and Scripts」第 109–155 段(text/29-fm-understanding-modules-and-scripts.txt:135,搜「playTrack(track」)。 ↩

  10. 出处:「Understanding Modules and Scripts」第 157–172 段(text/29-fm-understanding-modules-and-scripts.txt:172,搜「Track, playTrack」)。 ↩ ↩2

  11. 出处:「Understanding Modules and Scripts」第 174–176 段(text/29-fm-understanding-modules-and-scripts.txt:174,搜「not checked against」)。 ↩ ↩2

  12. 出处:「Understanding Modules and Scripts」第 178–194 段(text/29-fm-understanding-modules-and-scripts.txt:182,搜「always set to auto」)。 ↩ ↩2 ↩3 ↩4

  13. 出处:「Understanding Modules and Scripts」第 218–242 段(text/29-fm-understanding-modules-and-scripts.txt:237,搜「declare const ALBUM_API」)。 ↩

  14. 出处:「Understanding Modules and Scripts」第 244–265 段(text/29-fm-understanding-modules-and-scripts.txt:265,搜「scoped to the musicUtils.ts module」)。 ↩

  15. 出处:「Understanding Modules and Scripts」第 267–276 段(text/29-fm-understanding-modules-and-scripts.txt:271,搜「declare const MY_CONSTANT」)。 ↩

  16. 出处:「Understanding Modules and Scripts」第 196–216 段(text/29-fm-understanding-modules-and-scripts.txt:212,搜「ambient contexts」)。 ↩ ↩2 ↩3

  17. 出处:「Understanding Modules and Scripts」第 521–533 段(text/29-fm-understanding-modules-and-scripts.txt:533,搜「must-have」)。 ↩ ↩2 ↩3 ↩4

  18. 出处:「Understanding Modules and Scripts」第 278–304 段(text/29-fm-understanding-modules-and-scripts.txt:291,搜「already ambient」)。 ↩

  19. 出处:「Understanding Modules and Scripts」第 306–328 段(text/29-fm-understanding-modules-and-scripts.txt:315,搜「duration-utils」);「不对照、要手动同步」见第 328 段(搜「keep them up to date」)。 ↩ ↩2

  20. 出处:「Understanding Modules and Scripts」第 330–354 段(text/29-fm-understanding-modules-and-scripts.txt:345,搜「Adding an export」)。 ↩ ↩2 ↩3

  21. 出处:「Understanding Modules and Scripts」第 356–373 段(text/29-fm-understanding-modules-and-scripts.txt:366,搜「completely overridden」)。 ↩ ↩2 ↩3

  22. 出处:「Understanding Modules and Scripts」第 352、371 段(text/29-fm-understanding-modules-and-scripts.txt:371,搜「changing express.d.ts to express.ts」)。 ↩

  23. 出处:「Understanding Modules and Scripts」第 379–413 段(text/29-fm-understanding-modules-and-scripts.txt:396,搜「callbackfn」)、第 409 段(搜「lib.es2021.string.d.ts」)。 ↩ ↩2

  24. 出处:「Understanding Modules and Scripts」第 415–426 段(text/29-fm-understanding-modules-and-scripts.txt:426,搜「inherits from the target」)。 ↩

  25. 出处:「Understanding Modules and Scripts」第 428–455 段(text/29-fm-understanding-modules-and-scripts.txt:436,搜「declare var document」)。 ↩

  26. 出处:「Understanding Modules and Scripts」第 457–459 段(text/29-fm-understanding-modules-and-scripts.txt:459,搜「two major browsers」)。 ↩ ↩2

  27. 出处:「Understanding Modules and Scripts」第 461–485 段(text/29-fm-understanding-modules-and-scripts.txt:467,搜「types key」)。 ↩

  28. 出处:「Understanding Modules and Scripts」第 487–519 段(text/29-fm-understanding-modules-and-scripts.txt:507,搜「npm install --save-dev」)。 ↩ ↩2

  29. 出处:「Understanding Modules and Scripts」第 535–539 段(text/29-fm-understanding-modules-and-scripts.txt:537,搜「won’t catch it」)。 ↩ ↩2

  30. 出处:「Understanding Modules and Scripts」第 541–567 段(text/29-fm-understanding-modules-and-scripts.txt:555,搜「interface Document」)。 ↩ ↩2 ↩3

  31. 出处:「Understanding Modules and Scripts」第 569–592 段(text/29-fm-understanding-modules-and-scripts.txt:586,搜「*.png」)。 ↩ ↩2 ↩3

  32. 出处:「Understanding Modules and Scripts」第 594–614 段(text/29-fm-understanding-modules-and-scripts.txt:614,搜「as few declaration files as possible」)。 ↩ ↩2 ↩3 ↩4

  33. 出处:「Understanding Modules and Scripts」第 616–626 段(text/29-fm-understanding-modules-and-scripts.txt:622,搜「implicit dependencies」)。 ↩ ↩2 ↩3

  34. 出处:「Understanding Modules and Scripts」第 628–662 段(text/29-fm-understanding-modules-and-scripts.txt:658,搜「myFunc(): string」)。 ↩ ↩2

  35. 出处:「Understanding Modules and Scripts」第 664–704 段(text/29-fm-understanding-modules-and-scripts.txt:689,搜「declare const DEBUG」)。 ↩ ↩2

  36. 出处:「Understanding Modules and Scripts」第 706–742 段(text/29-fm-understanding-modules-and-scripts.txt:734,搜「interface Window」)。 ↩

  37. 出处:「Understanding Modules and Scripts」第 744–763 段(text/29-fm-understanding-modules-and-scripts.txt:753,搜「declare global」)。 ↩ ↩2 ↩3

  38. 出处:「Understanding Modules and Scripts」第 765–803 段(text/29-fm-understanding-modules-and-scripts.txt:795,搜「declare namespace NodeJS」)。 ↩ ↩2 ↩3

  39. 出处:「Understanding Modules and Scripts」第 803 段(text/29-fm-understanding-modules-and-scripts.txt:803,搜「merely a contract」)。 ↩ ↩2

  40. 出处:「Understanding Modules and Scripts」第 805–815 段(text/29-fm-understanding-modules-and-scripts.txt:809,搜「local scope while scripts」)。 ↩

On this page