1.1 核心概念与设计哲学


1.1 核心概念与设计哲学

在全书知识地图里,这一节处在最左上角,是所有后续内容的锚点。如果只能记住一句话,就是标题那四个字:类型即契约。但"契约"这个词太抽象,我们拆成三种你能立刻用在代码里的具体含义。

一、契约是给调用方看的"承诺"

先看一个没有契约的 JS 函数。

// 没有类型的世界:调用方只能靠文档或猜 function createUser(input) { return { id: Math.random(), name: input.nm, age: input.a }; } // 调用方写了 input.name,但函数读的是 input.nm —— 运行时才暴露,拿到 undefined const u = createUser({ name: "张三", age: 20 }); console.log(u.name); // undefined

同一逻辑加上类型,契约就显式了:

// 契约写进签名:我要的是 name(字符串) 和 age(数字),还给你一个带 id 的对象 interface UserInput { name: string; age: number; } interface User { id: number; name: string; age: number; } function createUser(input: UserInput): User { return { id: Date.now(), name: input.name, age: input.age }; } // 输入写错字段,保存的那一刻编译器就标红 const u = createUser({ name: "张三", age: 20 }); // OK const bad = createUser({ nm: "李四", a: 20 }); // 报错:对象字面量只能指定已知属性

输入输出对比:第一段代码在浏览器里静默产出 undefined,第二段在敲代码时就被 tsc 挡下。契约的价值不在"描述",在于它能被机器校验——这是它和普通注释的本质区别。

二、契约把"错误关口"从运行时前移到编译期

我们做后端时常说"输入校验要在边界做"。TypeScript 把这件事从运行时的 if 判断,提到了编译时的静态检查。这不是语法糖,是排错成本的数量级差异:运行时错误出现在用户那条请求上,编译期错误出现在你改代码的这一秒。

// 案例:订单状态机 type OrderStatus = "pending" | "paid" | "shipped" | "done"; function next(status: OrderStatus): OrderStatus { switch (status) { case "pending": return "paid"; case "paid": return "shipped"; case "shipped": return "done"; case "done": return "done"; } } // 如果将来加一个 "refunded" 状态却忘了在 switch 里处理, // 加上 exhaustiveness check 后编译直接失败(见下方代码块)
// 穷尽性检查:用 never 让编译器替你盯漏 function nextSafe(status: OrderStatus): OrderStatus { switch (status) { case "pending": return "paid"; case "paid": return "shipped"; case "shipped": return "done"; case "done": return "done"; default: { const _exhaustive: never = status; // 若加了新状态忘了处理,这里报错 return _exhaustive; } } }

背景:状态机是电商、工单系统的常客。操作:用联合类型 + never 兜底。结果:新增状态忘处理会编译失败。解读:契约在这里等于"状态迁移的完备性保证"。变式:把 next 改成返回 { status, reason },契约同样能描述。

三、契约是"可演进"的,不是"锁死"的

有人怕加类型会绑死需求变更。其实类型系统本身支持渐进收紧:先 any 跑通,再局部标注,最后开严格模式。契约是逐步签署的,不是一次性公证。

// 阶段一:宽松,先让项目跑起来 let config: any = loadConfig(); // 不报错,但失去保护 // 阶段二:收窄成具体形状 interface AppConfig { port: number; dbUrl: string; debug: boolean; } let config2 = loadConfig() as AppConfig; // 显式断言一次,后续享受检查 // 阶段三:让 loadConfig 直接返回 AppConfig,去掉断言 function loadConfig(): AppConfig { /* ... */ return { port: 3000, dbUrl: "...", debug: false }; }

这张图把"契约的三重角色"做个对照:

01-01-fig01-2

为什么不是"加了类型的 JavaScript"就够了

这个常见说法漏掉了关键一点:TypeScript 的类型是结构化的,不是名义化的(详见第二章)。class Cat{ name: string } 在 TS 眼里,只要形状一致就能互相赋值,类名不重要。这让契约描述的是"能力"而非"身份",特别适合跨模块、跨团队的松耦合协作——你不需要 import 我的类,只要你的对象长那样,契约就成立。

工程取舍:严格度的代价

strict 模式(第二章细讲)会强迫你给每个可能为 null 的地方标注。小项目开它事半功倍;百万行老项目一次性全开,可能冒出上万个错误。我们的经验是:新仓库直接 strict: true,老仓库从 strictNullChecks 单点开启,逐个模块吃。契约的覆盖度可以渐进,但别长期停在 any 上——那等于没签。

本节要点回顾

  • 契约的三重含义:给调用方的承诺、错误关口前移、可渐进演进
  • 类型信息在编译后被擦除,运行时不存在,所以契约只保护"写代码"这一阶段
  • 结构化类型让契约描述能力而非身份
  • 严格度可渐进,但不建议长期 any

⚠️ 不要把类型当成"运行时校验器"。TS 编译后类型消失,真正要在边界拦非法输入,仍需运行时 schema 校验(如 zod),两者职责不同。

💡 写新函数时先写签名再写实现,往往比先写实现再补类型更容易逼出清晰契约。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U