TypeScript联合类型含子类型成员时为何失去类型安全性?
如果我创建不同结构的联合类型,通常运行表现符合预期:
type BookT = { id: string; title: string; price: number; }; type MagazineT = { id: string, abstract: string }; type ReadingMaterialT = BookT | MagazineT; const readingMaterial: ReadingMaterialT[] = [ // 符合预期:TS提示缺少abstract字段 { id: '1', }, // 符合预期:TS提示缺少title字段 { id: '2', price: 9, }, // 符合预期:TS提示缺少price字段 { id: '3', title: 'foo', } ];
但如果联合类型中的一个结构是另一个的子类型(如下例中MagazineT是BookT的子类型),联合类型的表现就不再符合预期。实际上TypeScript允许往readingMaterial数组中添加既不属于BookT也不属于MagazineT类型的结构:
type BookT = { id: string; title: string; price: number; }; type MagazineT = { id: string }; type ReadingMaterialT = BookT | MagazineT; const readingMaterial: ReadingMaterialT[] = [ // 符合预期 { id: '1' }, // 不符合预期:应当提示缺少title字段! { id: '2', price: 9, }, // 不符合预期:应当提示缺少price字段! { id: '3', title: 'foo', } ];
请问为什么第二种场景下TypeScript编译器基本丧失了类型安全保障?还是说我第一个场景的正常表现只是偶然,并不推荐使用这种方式构造联合类型?
注:我清楚可以给每个类型添加字面量类型字段(例如type: 'book'和type: 'magazine')来构造可辨识联合(这也是该场景下的推荐实现方案),但我还是想了解编译器该行为的底层原因。
该行为是TypeScript的设计特性,和三个核心规则直接相关:
1. 结构化类型系统
TypeScript采用结构化类型判定规则,只要一个对象包含某类型要求的所有必填字段、且字段类型匹配,就会被判定为该类型的子类型,允许额外存在未在类型中声明的字段。
比如第二个例子中的{id: '2', price: 9}完全满足MagazineT仅要求id: string的结构要求,天然可以赋值给MagazineT类型的变量。
2. 联合类型的赋值规则
对于联合类型T1 | T2 | ... | Tn,只要值可以赋值给任意一个联合成员类型,就可以赋值给该联合类型。
第二个例子中的ReadingMaterialT = BookT | MagazineT,只要值匹配BookT或者MagazineT任一的结构要求,就可以合法赋值。你写的{id: '2', price: 9}虽然不满足BookT的要求,但完全满足MagazineT的要求,因此被判定为合法。
3. 多余属性检查的触发限制
你预期的"不允许存在类型未声明的字段"属于TypeScript的多余属性检查能力,该能力仅在「对象字面量直接赋值给明确标注了非联合、非宽泛类型的变量」时才会触发。
如果对象字面量已经匹配了联合类型中的任一成员的最小结构要求,TS就会跳过多余属性检查,默认你是主动为该子类型添加额外字段,不会判定为错误。
关于第一个例子的表现
第一个例子的报错不是偶然:两个联合成员BookT和MagazineT不存在子类型关系,你写的不完整对象无法匹配任何一个联合成员的结构要求,因此会触发报错。
如果需要严格限制联合类型的赋值必须完全匹配其中某一个成员的结构,可辨识联合是目前最稳定、可读性最高的实现方案。
内容的提问来源于stack exchange,提问作者Andru

