结构化类型VS标称类型:TS类型系统全解析
1. 结构化类型系统与标称类型系统
1. 结构化类型系统
结构化类型系统(又称鸭子类型)通过类型的结构(属性和方法) 判断兼容性,而非类型名称。只要两个类型的结构一致,就会被视为兼容。
class Cat { eat() {} }
class Dog { eat() {} }function feedCat(cat: Cat) {}
feedCat(new Dog()); // 不报错,因结构一致
class Cat { meow() {}; eat() {} } // 新增独特方法
class Dog { eat() {} }function feedCat(cat: Cat) {}
feedCat(new Dog()); // 报错,Dog缺少meow方法
class Cat { eat() {} }
class Dog { bark() {}; eat() {} } // 比Cat多一个方法function feedCat(cat: Cat) {}
feedCat(new Dog()); // 不报错,Dog完全包含Cat的结构
class Cat { eat(): boolean { return true; } }
class Dog { eat(): number { return 599; } }function feedCat(cat: Cat) {}
feedCat(new Dog()); // 报错,eat返回值类型不同2. 标称类型系统
标称类型系统通过类型名称判断兼容性,即使结构完全一致,名称不同也会被视为不同类型。常用于区分语义不同但基础类型相同的值(如货币单位)。
type USD = number; // 美元
type CNY = number; // 人民币const usd: USD = 100;
const cny: CNY = 200;function addCNY(a: CNY, b: CNY) { return a + b; }
addCNY(cny, usd); // 结构化类型中不报错(不合理),标称类型中会报错3. TS模拟标称类型系统
TypeScript默认是结构化类型系统,可通过附加独特标记模拟标称类型,让结构一致的类型被视为不同。
通过交叉类型给原始类型添加“隐藏标签”,仅在类型层面生效,不影响运行时。
// 定义带标签的工具类型
type Nominal<T, Tag extends string> = T & { __tag: Tag };// 应用到货币类型
type USD = Nominal<number, "USD">;
type CNY = Nominal<number, "CNY">;const usd: USD = 100 as USD;
const cny: CNY = 200 as CNY;function addCNY(a: CNY, b: CNY) { return (a + b) as CNY; }
addCNY(cny, usd); // 报错,类型不兼容(符合预期)通过类的私有属性作为标记,利用TypeScript对私有属性的检查机制区分类型,支持运行时逻辑。
class USD {private __tag!: void; // 私有属性作为标记constructor(public value: number) {}
}class CNY {private __tag!: void; // 同名称私有属性,但属于不同类constructor(public value: number) {}
}const usd = new USD(100);
const cny = new CNY(200);function addCNY(a: CNY, b: CNY) { return a.value + b.value; }
addCNY(cny, usd); // 报错,usd是USD类型(符合预期)总结
- 结构化类型系统:基于类型结构判断兼容性,广泛用于TypeScript、Python等语言。
- 标称类型系统:基于类型名称判断兼容性,用于C++、Java等语言,可避免语义冲突(如货币单位)。
- 模拟标称类型:在TypeScript中通过“附加标记”(类型交叉或类私有属性)实现,提升类型安全性。
2.TS类型系统层级
判断类型兼容性的方式
介绍了两种判断类型兼容性的方法,条件类型通过extends判断,变量赋值通过能否赋值判断子类型关系。
type Result = 'linbudu' extends string ? 1 : 2; // 1,说明'linbudu'是string的子类型
declare let source: string;
declare let anyType: any;
declare let neverType: never;
anyType = source; // 成立,string是any的子类型
// neverType = source; // 报错,string不是never的子类型从原始类型开始
原始类型(如string、number)与其对应的字面量类型(如"linbudu"、1)存在父子关系,字面量类型是原始类型的子类型;对象类型相关的字面量(如{}、[])也是object的子类型。
type Result1 = "linbudu" extends string ? 1 : 2; // 1
type Result2 = 1 extends number ? 1 : 2; // 1
type Result4 = { name: string } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1向上探索,直到穹顶之上
字面量类型是包含它的联合类型的子类型,原始类型是包含它的联合类型的子类型;同一基础类型的字面量联合类型是该基础类型的子类型。
type Result7 = 1 extends 1 | 2 | 3 ? 1 : 2; // 1
type Result10 = string extends string | false | number ? 1 : 2; // 1
type Result11 = 'lin' | 'bu' | 'budu' extends string ? 1 : 2; // 1原始类型(如string)是其装箱类型(如String)的子类型,装箱类型是Object的子类型;结构化类型系统中,String是{}的子类型,{}是object的子类型等,存在特殊兼容关系。
type Result14 = string extends String ? 1 : 2; // 1
type Result15 = String extends {} ? 1 : 2; // 1
type Result16 = {} extends object ? 1 : 2; // 1any和unknown是类型层级的顶端,Object是它们的子类型;any比较特殊,any extends T会返回1|2(同时包含成立和不成立的情况),且any与unknown互相兼容。
type Result22 = Object extends any ? 1 : 2; // 1
type Result23 = Object extends unknown ? 1 : 2; // 1
type Result24 = any extends Object ? 1 : 2; // 1 | 2
type Result31 = any extends unknown ? 1 : 2; // 1向下探索,直到万物虚无
never是类型层级的最底层,是任何类型的子类型;null、undefined、void是独立类型,不是其他原始类型的子类型(严格模式下)。
type Result33 = never extends 'linbudu' ? 1 : 2; // 1
type Result34 = undefined extends 'linbudu' ? 1 : 2; // 2
type Result35 = null extends 'linbudu' ? 1 : 2; // 2组合上述结论可形成完整的类型层级链,从never向上依次为:字面量类型 → 包含该字面量的联合类型 → 原始类型 → 装箱类型 → Object → any/unknown,所有层级关系可通过嵌套条件类型验证。
type TypeChain = never extends 'linbudu'? 'linbudu' extends 'linbudu' | '599'? 'linbudu' | '599' extends string? string extends String? String extends Object? Object extends any? any extends unknown? unknown extends any? 8: 7: 6: 5: 4: 3: 2: 1: 0; // 结果为8,所有条件成立其他比较场景
基类与派生类:派生类是基类的子类型(因保留基类结构并新增内容)。
联合类型子集:若联合类型A的所有成员都在联合类型B中,则A是B的子类型。
type Result36 = 1 | 2 | 3 extends 1 | 2 | 3 | 4 ? 1 : 2; // 1
type Result38 = 1 | 2 | 5 extends 1 | 2 | 3 | 4 ? 1 : 2; // 2数组和元组:元组成员类型符合数组类型时,元组是数组的子类型;空数组是任意数组的子类型等。
type Result40 = [number, number] extends number[] ? 1 : 2; // 1
type Result43 = [] extends number[] ? 1 : 2; // 1总结与预告
本文通过从原始类型向上、向下探索,明确了TypeScript的类型层级(从never到any/unknown),涵盖了联合类型、装箱类型等特殊情况的兼容性规则。学习类型层级是理解条件类型的基础,下一节将讲解条件类型及infer等概念。
