Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/TypeScript 深度教程/第11章

第11章:兼容性与推断 —— 结构类型、方差与类型关系

4 分钟 · 更新于 2026-09-01

第11章:兼容性与推断 —— 结构类型、方差与类型关系

本章补齐官方 Handbook 中容易被基础教程略过的底层规则:类型为什么兼容、函数参数为什么特殊、泛型如何影响关系、推断怎样选择类型,以及来自 Java/C# 或函数式语言的开发者需要转换哪些心智模型。


一、结构类型系统

TypeScript 主要比较结构而不是声明名称:

ts
interface Point {
  x: number;
  y: number;
}

class Coordinate {
  constructor(public x: number, public y: number) {}
}

const point: Point = new Coordinate(1, 2);

只要所需成员兼容,来源名称和继承关系通常不重要。

二、额外属性与赋值

ts
const value = { x: 1, y: 2, label: "origin" };
const point: Point = value; // 允许

但对象字面量直接赋值会执行额外属性检查:

ts
const point: Point = { x: 1, y: 2, label: "origin" }; // 报错

额外属性检查主要用于捕捉拼写错误,不代表 TypeScript 改成了名义类型系统。

三、函数兼容性

返回值可以更具体:

ts
type CreateAnimal = () => Animal;
const createDog = (): Dog => new Dog();
const createAnimal: CreateAnimal = createDog;

参数方向更谨慎。若目标可能传入任意 Animal,只会处理 Dog 的函数就不安全。

严格函数类型有助于检查这种逆变关系,但方法位置为兼容既有 JavaScript 模式仍有特殊规则。不要只靠术语判断,应该从“调用者可能传什么”推演。

四、可选参数与剩余参数

JavaScript 允许忽略额外参数,因此回调兼容规则较宽松:

ts
[1, 2, 3].forEach(value => console.log(value));

回调声明中不要因为调用方可能忽略参数,就把库一定会提供的参数标为可选。可选意味着实现必须处理参数缺失。

五、重载兼容

带重载的函数要与目标的每个所需签名兼容。重载应从具体到宽泛排列,且实现签名必须覆盖全部分支。

ts
function format(value: Date): string;
function format(value: number): string;
function format(value: Date | number): string {
  return value instanceof Date ? value.toISOString() : String(value);
}

六、类兼容性

类的实例侧按结构比较;构造函数和静态成员通常不参与实例赋值比较。privateprotected 会引入来源要求:拥有私有成员的两个类,只有成员来自同一声明体系时才可能兼容。

这使类在特定情况下具有部分名义类型特征。

七、泛型兼容性

泛型参数是否影响成员结构,决定不同实例是否可区分:

ts
interface Empty<T> {}
let a: Empty<number>;
let b: Empty<string>;
a = b; // 结构相同
ts
interface Box<T> { value: T }
let numberBox: Box<number>;
let stringBox: Box<string>;
// 不兼容

没有进入结构的类型参数无法影响兼容性。

八、协变、逆变与不变

实用理解:

  • 只产出 T 的容器倾向协变。
  • 只消费 T 的回调倾向逆变。
  • 同时读写 T 时通常需要更谨慎,可能应视为不变。

官方泛型还支持方差注解用于极少数高级库场景,但通常应让编译器从结构推断,不要把注解当作强制修改兼容关系的工具。

九、类型系统并非完全健全

TypeScript 为兼容 JavaScript 和开发体验接受部分不健全规则,例如某些函数参数双向兼容、可变数组关系等。重要结论:

“TypeScript 允许赋值”不等于数学意义上绝对安全,公共库和高风险边界仍需保守设计。

十、类型推断

最佳公共类型

ts
const values = [1, 2, null];

编译器会从候选类型和上下文中寻找可兼容结果。

上下文类型

ts
window.addEventListener("click", event => {
  // event 根据 addEventListener 的签名推断
});

类型信息不仅从表达式向外流,也会从目标位置向内流。

十一、不同编程背景的心智转换

Java/C# 开发者

  • 不要假设类名决定类型身份。
  • 接口无需类显式声明实现也可结构兼容。
  • 类型在运行时通常被擦除,无法任意反射。
  • 静态类常可由模块与普通函数替代。

函数式开发者

  • 联合类型近似和类型,交叉类型近似同时满足多个约束。
  • 判别联合与模式匹配思路相近。
  • TypeScript 没有通用高阶类型,类型级抽象有边界。
  • 结构类型和渐进类型意味着规则比纯函数式语言更宽松。

十二、空类型与相同结构

ts
class EmptyA {}
class EmptyB {}

let a: EmptyA = new EmptyB();

空类没有可区分结构,因此兼容。若需要身份区分,可使用私有品牌字段或品牌类型,但应提供受控构造方式。

十三、常见误区

  1. 认为同名接口才兼容。
  2. 把额外属性检查误解为永久禁止额外字段。
  3. 回调参数全部写成可选。
  4. 泛型参数未进入结构,却期待它区分类型。
  5. 把数组协变当作完全安全规则。
  6. 依赖类名进行领域身份区分。
  7. 从其他语言照搬运行时反射和名义类型假设。
  8. 看到编译通过就认为不存在任何不健全风险。

十四、练习

  1. 创建两个结构相同但名称不同的接口并测试赋值。
  2. 比较对象字面量和中间变量的额外属性检查。
  3. 设计一个只读生产者和只写消费者,观察类型关系。
  4. 让泛型参数进入成员结构,比较兼容性变化。
  5. 用品牌类型区分两个底层都是字符串的 ID。

十五、总结

  1. TypeScript 主要使用结构类型系统。
  2. 函数参数与返回值具有不同的兼容方向。
  3. 私有成员会给类引入部分名义约束。
  4. 泛型只有进入实际结构才影响兼容性。
  5. 推断同时利用表达式和上下文信息。
  6. TypeScript 为 JavaScript 生态接受部分不健全规则,边界设计仍需保守。

请继续阅读:第12章:模块深潜。


原始资料引用


  • 第10章:综合实战
  • 第12章:模块深潜

本章目录
一、结构类型系统二、额外属性与赋值三、函数兼容性四、可选参数与剩余参数五、重载兼容六、类兼容性七、泛型兼容性八、协变、逆变与不变九、类型系统并非完全健全十、类型推断十一、不同编程背景的心智转换十二、空类型与相同结构十三、常见误区十四、练习十五、总结原始资料引用Related Documents
苏ICP备2025204887号-2