← 返回题目列表

TypeScript 的结构化类型系统是什么?

高频 中等 第 9 / 32 题 更新于 2026/07/29
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,但它至少有 xy。 因此它能赋给 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 } 的例子说明兼容,再补对象字面量额外属性检查和品牌类型,面试官追到工程边界也接得住。