TypeScript 使用 结构性类型(structural typing),或者称为 "鸭子类型"。对于类型的兼容性,结构(签名)是重要的,而不是类型的名称或来源。
示例:
interface Point2D { x: number; y: number; } interface Coord2D { x: number; y: number; } // 这些类型是可互换的:Point2D 和 Coord2D,因为结构是相同的。 const foo: Point2D = { x: 1, y: 2 }; const bar: Coord2D = foo; // OK!
命名性类型(nominal typing):对于类型的兼容性,重要的是 "名称" 或 "工厂",结构并不重要。
在 TypeScript 中,命名性类型并不是默认支持的,但可以通过品牌类型(branded types)来模拟:
type USD = number & { readonly __brand: unique symbol } type EUR = number & { readonly __brand: unique symbol } let priceUSD: USD; let priceEUR: EUR; // priceUSD = priceEUR; // 错误!不同的品牌。
为什么要应用这个? 例如,为了区分结构相同但语义上不同的类型 — 货币、用户 ID/tokenID、物理量等。
问题: 为什么以下代码可以编译而没有错误,尽管 Address 和 UserId 在逻辑上是不同的类型?
interface Address { value: string; } interface UserId { value: string; } let id: UserId = { value: "test" }; let addr: Address = id; // OK
回答: 因为在 TypeScript 中,结构才是重要的,而不是类型的名称。这两种类型——只是一个 "具有 value: string 的对象"。
故事
项目: 一个使用 USD/EUR 计算的金融系统。金额通过 number 传递。有一次在加法中混淆了货币 — 由于 TypeScript 的结构性类型未能发现这个错误。后来引入了品牌类型,以便在编译阶段排除此类错误。
故事
项目: 在开发 REST API 时,使用对象表示不同实体的 ID(userId、groupId),两者具有 value: string 字段。错误地将 userId 用作 groupId,只有服务器上的业务逻辑才发现了这个错误。
故事
项目: 在 DSL 的解析库中,使用相同的结构 (type value = { kind: 'num'|'str', value: number|string })。类型相同的结构在代码的不同部分混合在一起,导致逻辑错误。添加了人工品牌字段以进行区分。