编程TypeScript 架构师

TypeScript 中结构性(结构性)类型与命名性类型的区别是什么?是否可以实现命名性类型,如果可以的话 — 怎么做?这可以解决什么问题?

用 Hintsage AI 助手通过面试

回答

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 })。类型相同的结构在代码的不同部分混合在一起,导致逻辑错误。添加了人工品牌字段以进行区分。