第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);
}
六、类兼容性
类的实例侧按结构比较;构造函数和静态成员通常不参与实例赋值比较。private 和 protected 会引入来源要求:拥有私有成员的两个类,只有成员来自同一声明体系时才可能兼容。
这使类在特定情况下具有部分名义类型特征。
七、泛型兼容性
泛型参数是否影响成员结构,决定不同实例是否可区分:
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();
空类没有可区分结构,因此兼容。若需要身份区分,可使用私有品牌字段或品牌类型,但应提供受控构造方式。
十三、常见误区
- 认为同名接口才兼容。
- 把额外属性检查误解为永久禁止额外字段。
- 回调参数全部写成可选。
- 泛型参数未进入结构,却期待它区分类型。
- 把数组协变当作完全安全规则。
- 依赖类名进行领域身份区分。
- 从其他语言照搬运行时反射和名义类型假设。
- 看到编译通过就认为不存在任何不健全风险。
十四、练习
- 创建两个结构相同但名称不同的接口并测试赋值。
- 比较对象字面量和中间变量的额外属性检查。
- 设计一个只读生产者和只写消费者,观察类型关系。
- 让泛型参数进入成员结构,比较兼容性变化。
- 用品牌类型区分两个底层都是字符串的 ID。
十五、总结
- TypeScript 主要使用结构类型系统。
- 函数参数与返回值具有不同的兼容方向。
- 私有成员会给类引入部分名义约束。
- 泛型只有进入实际结构才影响兼容性。
- 推断同时利用表达式和上下文信息。
- TypeScript 为 JavaScript 生态接受部分不健全规则,边界设计仍需保守。
请继续阅读:第12章:模块深潜。
原始资料引用