第 10 章 读懂一个大仓
代码基线:commit
a89bca1316(2026-10-06)| DSH0.2.0-rc.1| 本系列讲次:T9收口章。前九章的工具箱,在这一章变成一套流程。
本章导读
这一章回答你最初的问题:「我想通过 DSH 全面掌握 TS,有可能吗?」
答案前九章已经给完了——第 01 章说类型不存在,第 07 章说接口能被扩展,
第 05 章说类型管不到的地方要靠门禁。这一章要回答的是更实际的那一半:
面对一个 7507 个 TypeScript 文件、你完全陌生的代码库,
从零到能改它,该按什么顺序看?
这件事没有任何教程教过,因为它不是语言问题,是工程问题。
而它恰恰是你问的"通过一个项目掌握 TS"的真正答案所在。
本章不引入新语法。它交付一套流程,并且那套流程会被写成一个能跑的脚本——
[labs/M10-navigate-lab.ts``labs/M10-navigate-lab.ts,它能在十秒内给一个陌生包
画出公开 API 面。
读完本章你要能回答:"我该先读哪个文件?"
答案会让你意外:不是 index.ts,也不是入口文件。
一、为什么没有教程教这个
因为传统教程的组织方式是按知识点:泛型一章、条件类型一章。
它默认你已经在一个能跑的、小到你已经熟悉的项目里练习。
而真实世界的入口不是这样的。真实入口是:
一个你没用过的库
└── 一个你看不懂的报错
└── 350,000 行 TypeScript
└── 372 个 tsconfig,2736 处 declare module
└── 没有文档,或者文档有 1 万行
从"报错"到"能改"之间的这段路,没有教材。
但这段路 100% 由类型系统的性质决定。因为类型系统有一个别的工具没有的性质:
它是一张可执行的地图。 名字在哪、边界在哪、谁依赖谁——
全都能从类型声明里读出来,不需要理解实现。
本章的流程就是反复利用这一点:把代码库变成一张可导航的类型图。
本章第一条纪律
读一个陌生仓库时,先看类型声明,不要先看实现。
实现会骗你(可能有 bug、可能有兼容包袱),类型不会——
类型是这个模块承诺给世界的东西,而承诺通常比实现更可靠、更稳定。
二、第 0 步:规则先于代码
这一步很多���不跳,但它省下的时间最多。
打开 DSH,第一件该读的不是 README.md,是
[AGENTS.md``AGENTS.md 和 [docs/architecture.md``docs/architecture.md。
DSH 的 AGENTS.md 有 130 多行,密度极高,每一条都是可执行的约束:
| 规则 | 它替你省下的时间 |
|---|---|
Model-visible ⟺ logged |
让你不去纠结"这个日志要不要记" |
No new assertions to unknown |
让你不去猜 as unknown 该不该用 |
Explicit > implicit at package boundaries |
让你不去纠结默认值写哪 |
Switch on discriminant tags |
让你知道 switch 该不该写 default |
| Opaque cross-boundary ids are branded | 让你知道 id 该不该用品牌类型 |
这些不是"建议",是别人已经踩完坑之后写下的答案。
在一个陌生代码库里,前人的坑位记录比代码本身更值钱。
这一步有个反直觉之处:
AGENTS.md里说的每一条规则你都会在本书里遇到对应的技术点。
它是一张索引——先读规则,你就有了一张"这个仓库在哪些方面有讲究"的地图。
三、第 1 步:从 tsconfig 画出依赖方向
DSH 有 372 个 tsconfig.json 声明了 references。这个数字说明一件事:
依赖方向是被显式声明的,不是靠读 import 猜的。
project references 是 TypeScript 的原生机制:一个 tsconfig 通过
references 列出它依赖的其它项目。它的价值是双向的:
- 正向:告诉你"我依赖谁"
- 反向:编译器知道依赖图,所以能只构建受影响的子集(
tsc -b)
对你这个读者,正向信息就是全部价值:
// packages/client/my-page/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": { "rootDir": "src", "outDir": "lib/types" },
"references": [
{ "path": "../../core/session" },
{ "path": "../ui-primitives" }
// ...
]
}
这一行信息回答了"这个包站在什么位置"。 不用打开任何源码。
而 extends 字段回答了另一件事:它继承哪套编译选项。
tsconfig.base.json 里那几项是整个仓库的地基:
{
"strict": true,
"noUncheckedIndexedAccess": true, // ← 第 01、03 章
"exactOptionalPropertyTypes": true,
"moduleResolution": "bundler"
}
读懂这三行,你就知道了这个仓库的一半性格。
第 03 章讲 noUncheckedIndexedAccess 时说的那句"它决定索引访问是否要处理缺失",
就是从这里来的。
本章第二条纪律
陌生仓库的第一张图是
tsconfig的references+extends,不是目录树。
references给依赖方向,extends给全局性格。两张图加起来,
你在不看任何源码的情况下就知道这个仓库"站在哪里"和"讲究什么"。
四、第 2 步:export 就是公开面
第二步:找出模块的承诺。
TypeScript 里,没有 export 的东西不是公开 API。这不只是约定——
不导出的话,外部根本编译不过引用它。
所以公开面的读取方法极其简单:读 index.ts(或包 exports 指向的入口)的导出声明。
看 [packages/util/brand/src/index.ts``packages/util/brand/src/index.ts:
39 行,4 个 export,于是它的全部承诺是:
export type Branded<B extends string>
export type BrandedNumber<B extends string>
export function brandString<T extends Branded<string>>(value: string | T): T
export function brandNumber<T extends BrandedNumber<string>>(value: number | T): T
读完这四行,你就完全理解了这个包。 加上它模块头部那段设计说明
("duplicate-install-safe"、"keeps no runtime identity or mutable state"),
你知道了它的动机和边界——这比读 300 行实现有用得多。
本系列的实验脚本 [labs/M10-navigate-lab.ts``labs/M10-navigate-lab.ts 做的
就是这件事:给定一个包,输出它的 references、extends 与全部导出声明。
对 DSH 的任何一个包都能用,十秒出结果。
4.1 但要小心 export *
export * from './x.ts' 会把整个子模块的公开面重新导出。
读到 export * 时要继续往下走,直到看到实际的声明。
这不是缺陷,是设计:它让一个包可以渐进地拆分内部文件,
而公开面保持稳定。但对读者来说,export * 是导航的断点——
你必须顺着它走才知道这个包到底承诺了什么。
本章第三条纪律
导航一个包时,把
export *当作断点而不是终点。
公开面的真实形状藏在链条末端。
五、第 3 步:门禁是最快的反馈回路
第三章讲过"会拒绝你的环境比会讲解的环境教得更多"。这里给出具体的做法。
DSH 的门禁分若干层,每层回答不同的问题:
| 层 | 命令 | 回答什么 |
|---|---|---|
| 最快 | npx vitest run <path> |
我改的这个函数,行为对不对 |
| 契约 | verify-*-* 系列门禁 |
我的写法符不符合这个仓库的规矩 |
| 全局 | pnpm run doc-sync |
我的改动是否让文档失真 |
| 构建 | pnpm run typecheck |
全仓库类型还通不通 |
关键认识:门禁不是"提交前的关卡",是"实时的编译器 + 静态检查员"。
本次写这本教程时,我个人被同一套门禁拦下过三次:
- 提交语料时
whitespace门禁拒绝(书稿里有行尾空格) - 再次提交时
lint因node不在 PATH 报 127 - 推送时
typecheck失败,因为 PATH 同样问题
每一次拦截都在零成本状态下发现了真问题。 这就是反馈回路的价值。
对陌生仓库的正确用法:在读代码之前先让门禁跑一遍。
绿灯说明你的环境对了;红灯会立刻告诉你缺什么(比如没装依赖)。
这是"验证自己对环境的理解"的最快方式,比读 README 快十倍。
本章第四条纪律
把门禁当作学习工具而不是审批流程。
写完一个文件就跑一次最窄的那条命令——
它给你的反馈比console.log快一百倍,而且不会骗你。
六、第 4 步:识别方言
这是最后一步,也是新手最容易忽略、后果最严重的一步。
DSH 的 TypeScript 是一种方言。 本书一路下来记录了至少六处它与主流写法的差异:
| DSH 的规矩 | 主流代码库通常 |
|---|---|
禁止新增 as unknown(第 02 章) |
用得很勤 |
noUncheckedIndexedAccess 全开(第 01、03 章) |
多数不开 |
| 回调用属性语法(第 02 章) | 两种都有 |
1034 处 declare module 增强(第 07 章) |
几乎没有 |
| 事件名双列表 + 生成器门禁(第 05 章) | 直接手写联合类型 |
verbatimModuleSyntax: false(第 01 章) |
更多项目开 true |
如果你只从 DSH 学,你会写得很"对",然后读别人的代码处处别扭。
第 02 章那句 as unknown 的例子不是学术讨论——它是真实的摩擦:
你在 DSH 里写了半年,回去看一个普通项目,会发现满屏的 as any。
本章第五条纪律
学一个方言代码库时,主动列一张"这里与外面不同"的清单。
本书的勘误总表附录 A 里有 59 条,其中相当一部分就是方言记录。
把差异写下来,你就同时拥有了两种写法的判断力。
七、实操:十分钟导航一个陌生包
把上面五步压成一个可执行的流程。本章的实验脚本就是它的实现。
# 给定一个你完全没见过的包
node --import tsx/esm "ts-源码精读/labs/M10-navigate-lab.ts" packages/util/brand
它输出四段:
- 依赖方向(
references) - 编译性格(
extends链上的关键compilerOptions) - 公开面(全部
export声明,逐字) - 内部文件清单(
src/下有什么,用来判断规模)
十分钟能拿到的结果,通常比你读一小时源码多。
7.0 写这个工具时踩到的第一脚
这个导航工具的第一版读不出 tsconfig 的内容。原因是:
tsconfig.json 是 JSONC,不是 JSON ——它允许注释,而 JSON.parse 不允许。
首版用正则剥离注释,结果连续失败两次:DSH 的 tsconfig.base.json 里
大量 glob 路径("@deepseek-ai/dsh-client-*/invariant": [...])与注释互相干扰,
//.* 这样的正则会误伤字符串内容。
最终解法是别自己写:TypeScript 自带
ts.parseConfigFileTextToJson(path, text)
它就是为 JSONC 设计的。
这是本章流程的一个真实注脚:你用"读类型声明"来导航一个仓库时,
第一个要跨过的技术门槛就是配置文件的格式不是你想的那种。
这在真实工程里是常态。
7.1 十分钟之后的你
你手上有了:依赖方向、编译性格、公开承诺、规模。
接下来读实现,你的读法已经完全不同了:
- 看到
ctx.effect(),你知道这是 AGENTS.md 说的"注册即效应" - 看到
as SessionId,你知道这里不该有as unknown(方言) - 看到一个
declare module,你知道它在给别人的类型加成员(第 07 章) - 想改一个公开签名,你知道会影响哪些
references指向它的包
这才是"读懂一个大仓"——不是读完,是获得导航能力。
八、本书收口:九章的工具箱
回头看这十章,它们其实是一套分层工具:
| 层 | 章 | 工具 | 回答什么 |
|---|---|---|---|
| 地基 | 01 | 擦除、类型空间 | 类型存不存在 |
| 基础 | 02 | 泛型、约束、推断 | 怎么让类型跟着值走 |
| 查询 | 03 | keyof / typeof / T[K] |
怎么读一个类型 |
| 判断 | 04 | 条件类型 / infer / 分发 |
怎么判断并提取 |
| 变换 | 05 | 映射 / 模板字面量 | 怎么遍历并重塑 |
| 收窄 | 06 | as const / satisfies |
怎么固定形状 |
| 扩展 | 07 | 声明合并 / 模块增强 | 怎么往别人的类型上加东西 |
| 边界 | 08 | 品牌类型 / 断言函数 | 类型在数据边界上失效时怎么办 |
| 时序 | 09 | Awaited / AsyncIterable |
怎么描述还没发生的事 |
| 工程 | 10 | 导航流程 | 怎么在陌生仓库里用上前九章 |
最后一层最重要。 因为前九层的每一件工具,在真实工作里的使用频率,
远低于"读懂我在哪个位置、这个仓库的规矩是什么"。
8.1 回到最初的问题
「我想通过 DeepSeek Harness 全面掌握 TS,有可能吗?」
全面掌握——不可能,任何单一材料都做不到。 第 05 章已经用
SessionEventMap 的例子说明过原因:这个仓库有它教不了你的东西
(生成器、门禁、运行时注册表的取舍),而别的仓库有它教不了你的别的东西。
但能做到的是:
- 类型系统这一层,DSH 是市面上最好的教材之一——
1034 处模块增强、186 处Awaited<ReturnType<...>>、
241 处品牌类型、50 个靠生成器与门禁同步的事件名。
这些在别处学不到,因为别处不需要。 - 工程约束这一层,DSH 也是最好的教材之一——
372 个显式依赖声明、AGENTS.md 里的 130 多条可执行规则、
以及那套"会拒绝你"的门禁。 - 日常 TS 与生态广度,DSH 教不了——React 只有 1753 处 hook,
Node 只有 1115 处导入,标准库几乎不碰。这部分要靠外部材料补。
所以最终的答案是:把 DSH 当脊椎,当压力测试场,当方言样本,
但别当教科书。它最强的地方不是教语法,是让你在一个真实的大型代码库里
被规则反复纠正——而那正是 TypeScript 能力增长最快的方式。
8.2 最后一句
第 07 章末尾说过:在一个"拼错 = 静默降级"的系统里,唯一可靠的防线是人工核对。
这句话同样适用于这本书:
本书的每一个论断都标了实验断言,预期输出随书入库,删掉重生成仍逐字节一致。
但没有任何工具能替你判断这些结论是否值得相信。
那是你要做的事。
本章实验(附录)
实验脚本:[labs/M10-navigate-lab.tslabs/M10-navigate-lab.ts
运行(在仓库根目录):
export PATH="/opt/homebrew/bin:$PATH"
cd /Users/ygs/ygs/deepseek-harness
node --import tsx/esm "ts-源码精读/labs/M10-navigate-lab.ts" packages/util/brand
它是什么:一个真实的导航工具,而不是一组断言。它对任意包路径都能工作,
输出该包的依赖方向、编译性格、公开面与规模。
预期输出(对 packages/util/brand 实测,完整输出见 labs/expected/M10-navigate-lab.out):
── 1. 依赖方向(tsconfig references)
(无 references —— 这是一个叶子包,不依赖任何工作区项目)
── 2. 编译性格(extends 链)
extends ../../../tsconfig.base.json
extends tsconfig.base.json
compilerOptions.strict = true
compilerOptions.noUncheckedIndexedAccess = true
compilerOptions.exactOptionalPropertyTypes = true
compilerOptions.verbatimModuleSyntax = false
compilerOptions.moduleResolution = bundler
compilerOptions.target = es2024
(以上是全仓库地基,来自 tsconfig.base.json)
── 3. 公开面(入口的 export 声明)
入口:packages/util/brand/src/index.ts
export type Branded<B extends string> = string & { readonly [BRAND]: B }
export type BrandedNumber<B extends string> = number & { readonly [BRAND]: B }
export function brandString<T extends Branded<string>>(value: string | T): T {
export function brandNumber<T extends BrandedNumber<string>>(value: number | T): T {
── 4. 规模(src/ 文件清单)
packages/util/brand/src/index.ts
合计:1 个文件,约 40 行
── 5. 工具自检
✓ 目标包存在
✓ 找到了 tsconfig
✓ extends 链能解析出仓库地基(tsconfig 是 JSONC,注释已剥离)
✓ 入口文件可读且非空
✓ 规模扫描得到文件数
✓ 对 brand 包能列出全部四条导出(本书第 02、08 章的锚点)
全部断言通过。
本章的勘误条目见 [附录 A · 勘误总表附录A-勘误总表.md。