联合类型
interface Bird {fly(): void;layEggs(): void;}interface Fish {swim(): void;layEggs(): void;}const getPet: () => Bird | Fish = () => {return {// ...} as Bird | Fish;};const Pet = getPet();Pet.layEggs(); // okPet.fly(); // ts(2339) 'Fish' 没有 'fly' 属性; 'Bird | Fish' 没有 'fly' 属性
使用基于 in 操作符判断的类型守卫
if (typeof Pet.fly === 'function') { // ts(2339)Pet.fly(); // ts(2339)}if ('fly' in Pet) {Pet.fly(); // ok}
交叉类型
{type Useless = string & number;}
很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Useless 的类型就是个 never。
合并接口类型
不同名属性,可以理解为求并集。
type IntersectionType = { id: number; name: string } & { age: number };const mixed: IntersectionType = {id: 1,name: "name",age: 18,};
如果同名属性的类型不兼容,属性交叉类型为never
type IntersectionTypeConfict = { id: number; name: string; }& { age: number; name: number; };const mixedConflict: IntersectionTypeConfict = {id: 1,name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型age: 2};
如果同名属性的类型兼容,合并后 name 属性的类型就是两者中的子类型。####
type IntersectionTypeConfict = { id: number; name: 2; }& { age: number; name: number; };let mixedConflict: IntersectionTypeConfict = {id: 1,name: 2, // okage: 2};mixedConflict = {id: 1,name: 22, // '22' 类型不能赋给 '2' 类型age: 2};
合并联合类型
我们也可以将合并联合类型理解为求交集。
type UnionA = 'px' | 'em' | 'rem' | '%';type UnionB = 'vh' | 'em' | 'rem' | 'pt';type IntersectionUnion = UnionA & UnionB;const intersectionA: IntersectionUnion = 'em'; // okconst intersectionB: IntersectionUnion = 'rem'; // okconst intersectionC: IntersectionUnion = 'px'; // ts(2322)const intersectionD: IntersectionUnion = 'pt'; // ts(2322)
既然是求交集,如果多个联合类型中没有相同的类型成员,交叉出来的类型自然就是 never 了,如下代码所示:
type UnionC = 'em' | 'rem';type UnionD = 'px' | 'pt';type IntersectionUnionE = UnionC & UnionD;const intersectionE: IntersectionUnionE = 'any' as any; // ts(2322) 不能赋予 'never' 类型
联合、交叉组合
联合、交叉类型本身就可以直接组合使用,这就涉及 |、& 操作符的优先级问题。实际上,联合、交叉运算符不仅在行为上表现一致,还在运算的优先级和 JavaScript 的逻辑或 ||、逻辑与 && 运算符上表现一致 。
联合操作符 | 的优先级低于交叉操作符 &,同样,我们可以通过使用小括弧 () 来调整操作符的优先级。
type UnionIntersectionA = { id: number; } & { name: string; } | { id: string; } & { name: number; }; // 交叉操作符优先级高于联合操作符type UnionIntersectionB = ('px' | 'em' | 'rem' | '%') | ('vh' | 'em' | 'rem' | 'pt'); // 调整优先级
type UnionIntersectionC = ({ id: number; } & { name: string; } | { id: string; }) & { name: number; };type UnionIntersectionD = { id: number; } & { name: string; } & { name: number; } | { id: string; } & { name: number; }; // 满足分配率type UnionIntersectionE = ({ id: string; } | { id: number; } & { name: string; }) & { name: number; }; // 满足交换律
在上述代码中,第 2 行是在第 1 行的基础上进行展开,说明 & 满足分配率;第 3 行则是在第 1 行的基础上调整了成员的顺序,说明 | 操作满足交换律。
类型缩减
type URStr = 'string' | string; // 类型是 stringtype URNum = 2 | number; // 类型是 numbertype URBoolen = true | boolean; // 类型是 booleanenum EnumUR {ONE,TWO}type URE = EnumUR.ONE | EnumUR; // 类型是 EnumUR
TypeScript 对这样的场景做了缩减,它把字面量类型、枚举成员类型缩减掉,只保留原始类型、枚举类型等父类型,这是合理的“优化”。
可是这个缩减,却极大地削弱了 IDE 自动提示的能力,如下代码所示:
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // 类型缩减成 string
在上述代码中,我们希望 IDE 能自动提示显示注解的字符串字面量,但是因为类型被缩减成 string,所有的字符串字面量 black、red 等都无法自动提示出来了。
不要慌,TypeScript 官方其实还提供了一个黑魔法,它可以让类型缩减被控制。如下代码所示,我们只需要给父类型添加“& {}”即可。
type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // 字面类型都被保留
当联合类型的成员是接口类型,如果满足其中一个接口的属性是另外一个接口属性的子集,这个属性也会类型缩减,如下代码所示:
type UnionInterce ={age: '1';} |({age: '1' | '2';[key: string]: string;});let a: UnionInterce = {age: '1','fs': 'f',33: 333 // 不能将类型“number”分配给类型“string”。ts(2322)}
如何定义如下所示 age 属性是数字类型,而其他不确定的属性是字符串类型的数据结构的对象?
{age: 1, // 数字类型anyProperty: 'str', // 其他不确定的属性都是字符串类型...}
这个问题的核心在于找到一个既是 number 的子类型,这样 age 类型缩减之后的类型就是 number;同时也是 string 的子类型,这样才能满足属性和 string 索引类型的约束关系。
type UnionInterce = {age: number,} | {age: never,[key: string]: string}let a: UnionInterce = {age: 1,name: 'zhl'}
