TS 类型系统速成:为读源码而学

专栏:TypeScript 与 Node 地基 · 第 2 / 13 篇
TypeScript类型系统

:::info 学习目标 完成本篇后你能够:读懂 dsh 源码里 90% 的类型标注;使用联合类型与类型收窄写出安全的分支逻辑;理解 interfacetypereadonly、类型断言各自的用途。 前置:任意语言的基本编程经验。预计时长: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 穷尽了所有分支后,最后的 defaultcall 的类型是 never——漏写一个分支编译器立刻报错。**

联合类型如何收窄

图表(ts-type-system.md)

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=穷尽检查)——类型即文档,类型即不变量

常见踩坑

  1. any 当万能药——any 关闭所有检查,读源码时看到 any 要警惕;新代码用 unknown(使用前必须收窄);
  2. interface 之间冲突没意识到——同名 interface 会合并而不是报错(这正是第 5 篇声明合并的基础,也是某些诡异 bug 的来源);
  3. 对联合类型直接访问非公共字段——先收窄再取字段。

随堂练习(带验收标准)

  1. 定义一个 Message 联合类型(user/assistant/tool 三种,各带不同字段),写一个 render(msg) 函数用 switch 收窄并穷尽所有分支。验收:删掉任一分支后编译报错;
  2. 把练习 1 的所有字段加上 readonly,尝试修改,确认编译报错;
  3. 打开 dsh 的 packages/core/session/src/types.ts,找到 TurnEndReason,数一数它有几种分支、各自带什么数据。

← 返回文章列表