TypeScript 的结构化类型系统是什么?
简化版
TypeScript 使用结构化类型系统,判断类型兼容主要看对象结构是否满足要求,而不是看类型名字是否相同。只要属性和方法形状匹配,一个对象就可以赋给目标类型。
详细版
示例:
interface User {
name: string;
}
const p = { name: 'Alice', age: 18 };
const u: User = p; // 可以
p 没有显式声明实现 User,但它有 name: string,所以兼容。
这和 Java、C# 等更偏名义类型的语言不同。
面试重点是讲清楚结构兼容、额外属性检查和函数参数兼容这些边界。
完整版教学
一、结构化类型看形状,不看名字
TypeScript 的类型兼容规则主要关注结构。 如果一个值拥有目标类型要求的成员,它通常就可以被当成目标类型使用。 这符合 JavaScript 的鸭子类型风格。
type Point = { x: number; y: number };
const obj = { x: 1, y: 2, label: 'A' };
const p: Point = obj;
这里 obj 多了 label,但它至少有 x 和 y。
因此它能赋给 Point。
结构化类型让 TS 很适合渐进式描述 JavaScript 对象。
二、额外属性检查只在特定场景更严格
很多同学会遇到一个疑惑:变量赋值可以,直接对象字面量却报错。 这是因为 TypeScript 对对象字面量有额外属性检查。 它用于捕获拼写错误和误传字段。
type User = { name: string };
const a = { name: 'Alice', age: 18 };
const u1: User = a; // 可以
const u2: User = { name: 'Alice', age: 18 }; // 可能报错
流程可以理解为:
对象字面量直接赋给目标类型
-> 触发额外属性检查
变量先承接再赋值
-> 按结构兼容判断
这个设计是折中:既保持结构化类型灵活,又尽量拦住明显拼写错误。
三、函数类型也按结构兼容
函数兼容主要看参数和返回值。
返回值需要满足目标返回类型。
参数位置涉及逆变、双变等更细规则,严格程度受 strictFunctionTypes 影响。
type Handler = (x: number) => string;
const fn = (x: number) => String(x);
const h: Handler = fn;
表面看两个函数没有同名类型声明。 但参数和返回值结构匹配,所以兼容。 这也是回调、事件处理、函数式工具能顺畅工作的基础。
四、类也会按实例结构比较
两个类只要实例公共成员结构一致,也可能互相兼容。 这点和名义类型语言差别很大。 但 private/protected 成员会引入特殊限制,让兼容关系更接近名义类型。
class A {
x = 1;
}
class B {
x = 1;
}
let a: A = new B(); // 可以
| 类型系统 | 判断依据 | 典型特点 |
|---|---|---|
| 结构化类型 | 成员形状 | 灵活,贴近 JS |
| 名义类型 | 类型声明关系 | 严格,强调显式继承或实现 |
TypeScript 默认选择结构化,是为了兼容 JavaScript 生态。
五、结构化类型的好处和代价
好处是灵活。 第三方对象、接口返回值、测试 mock 不需要显式继承也能使用。 代价是某些“概念上不同但结构相同”的值可能被误用。
type UserId = string;
type OrderId = string;
let userId: UserId = 'u1';
let orderId: OrderId = userId; // 可以,但业务上可能不希望
如果要区分同样是 string 的不同业务 ID,可以用品牌类型模拟名义类型。 例如给类型加一个不可见标记字段。
六、品牌类型可以补充语义边界
品牌类型是一种常见技巧,用交叉类型给基础类型加标记。 这样结构上不再只是普通 string。 它适合 ID、金额、单位等容易混用的业务类型。
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
记忆钩子:TS 默认问“长得像不像”,品牌类型是在类型上贴一张“身份证”。
品牌类型通常通过工厂函数创建,避免到处乱断言。 它不是运行时安全机制,而是类型层面的语义约束。
七、常见误区与追问
- 误区:interface 名字不同就不兼容。 TypeScript 主要看结构,不看接口名字。
- 误区:多属性对象永远不能赋给少属性类型。 变量赋值通常可以,对象字面量直接赋值会触发额外属性检查。
- 误区:结构化类型没有任何严格性。 private/protected、额外属性检查、strict 配置都会影响兼容规则。
- 追问:为什么 TS 采用结构化类型? 因为 JavaScript 对象天然按形状使用,结构化类型更适合渐进式类型标注。
- 追问:如何避免 UserId 和 OrderId 都是 string 被混用? 可以使用品牌类型模拟名义类型。
- 追问:类之间没有继承关系也能赋值吗? 如果实例公共结构兼容,通常可以。
八、加强记忆
结构化类型就记“看形状,不看户口”。回答时先用 { name, age } 赋给 { name } 的例子说明兼容,再补对象字面量额外属性检查和品牌类型,面试官追到工程边界也接得住。