TS 类型系统速成:为读源码而学
专栏:TypeScript 与 Node 地基 · 第 2 / 13 篇:::info 学习目标
完成本篇后你能够:读懂 dsh 源码里 90% 的类型标注;使用联合类型与类型收窄写出安全的分支逻辑;理解 interface、type、readonly、类型断言各自的用途。
前置:任意语言的基本编程经验。预计时长:60 分钟。
:::
TypeScript = JavaScript + 静态类型标注。它不改变 JS 的运行方式(编译后类型全部擦除),只在编码时帮你抓错。读源码时,类型标注就是最好的文档——这也是为什么大型项目都选 TS。
基础类型与推断
let count = 1 // 推断为 number,不必写 number
let name: string = 'dsh'
let tags: string[] = ['agent', 'llm']
let maybe: string | undefined = undefined // 可能为空
function add(a: number, b: number): number {
return a + b
}
要点:优先依赖推断,只在函数签名(输入/输出)和复杂结构上写类型——dsh 源码正是这个风格。
联合类型与收窄:读源码的第一道坎
联合类型表示”值是几种类型之一”,使用时必须**收窄(narrow)**确定具体是哪种:
type ToolCall =
| { kind: 'function'; name: string; args: string }
| { kind: 'custom'; name: string; input: string }
function execute(call: ToolCall): string {
switch (call.kind) { // 收窄:此后每个分支里 call 的类型被确定
case 'function':
return call.args // ✓ 这里只有 function 才有的字段
case 'custom':
return call.input
}
}
这是 agent 源码中最高频的模式——消息、事件、工具调用全都是联合类型,靠一个判别字段(kind/type)收窄。dsh 的 TurnEndReason(completed/aborted/blocked/…)就是典型:
type TurnEndReason =
| { kind: 'completed' }
| { kind: 'blocked' }
| { kind: 'error'; error: { message: string; code: string } }
error 分支多了负载字段,收窄后才能安全读取。** TS 还有一个兜底检查:switch 穷尽了所有分支后,最后的 default 里 call 的类型是 never——漏写一个分支编译器立刻报错。**
联合类型如何收窄
interface 与 type:两种声明方式
interface 与 type:两种声明方式
interface ToolDefinition { // interface:对象形状,可被声明合并扩展
name: string
description: string
execute(args: unknown, signal: AbortSignal): Promise<unknown>
}
type Id = string // type:什么都行(联合、原始值、映射)
type ReadOnlyTool = Readonly<ToolDefinition> // 工具类型:全部字段只读
选择建议:对象结构用 interface(可被扩展),其余用 type。dsh 两种都用——可扩展的(Context、Events)用 interface,工具联合类型用 type。
只读、可选与字面量类型
interface SessionEvent {
readonly type: string // 只读:赋值后不可改(对应"仅追加日志"的语义)
seq: number
data?: unknown // 可选字段
}
type Status = 'enabled' | 'disabled' | 'auto-disabled' // 字面量联合:状态机的标准写法
readonly 与字面量联合组合是状态机的标准写法——dsh 的渠道状态、轮次结束原因都这样定义。修改只读字段会在编译期报错,把”仅追加”这类架构约束直接编码进类型系统。
类型断言:你比编译器知道得多时
const el = document.getElementById('root') as HTMLElement // 断言:我确定它存在
const args = JSON.parse(input) as { path: string } // 常见:解析 JSON 后断言形状
断言是编译期的”闭嘴”,不做任何运行时检查——断言错了照样崩。读源码时看到 as,意思是”作者在此处向读者保证”;写代码时,断言前最好有真实校验(下一篇的 zod 就是干这个的)。
与 dsh 的对照
第 6 篇的 SessionEventMap 摘录里,你现在已经能读懂全部语法:接口、可选字段、字面量联合。dsh 用类型表达架构约束(只读=仅追加、联合=事件词汇、never=穷尽检查)——类型即文档,类型即不变量。
常见踩坑
- 把
any当万能药——any关闭所有检查,读源码时看到any要警惕;新代码用unknown(使用前必须收窄); - interface 之间冲突没意识到——同名 interface 会合并而不是报错(这正是第 5 篇声明合并的基础,也是某些诡异 bug 的来源);
- 对联合类型直接访问非公共字段——先收窄再取字段。
随堂练习(带验收标准)
- 定义一个
Message联合类型(user/assistant/tool 三种,各带不同字段),写一个render(msg)函数用 switch 收窄并穷尽所有分支。验收:删掉任一分支后编译报错; - 把练习 1 的所有字段加上
readonly,尝试修改,确认编译报错; - 打开 dsh 的
packages/core/session/src/types.ts,找到TurnEndReason,数一数它有几种分支、各自带什么数据。