当前位置:网站首页>TypeScript 学习【7】高级类型:联合类型与交叉类型
TypeScript 学习【7】高级类型:联合类型与交叉类型
2022-06-09 04:34:00 【大杯美式不加糖】
联合类型
联合类型(Unions)用来表示变量、参数的类型不是单一原子类型,而可能是多种不同的类型的组合。
我们主要通过 | 操作符分隔类型的语法来表示联合类型。这里,我们可以把 | 类比为 JavaScript 中的逻辑或 ||,只不过前者表示可能的类型。
举个例子,我们封装了一个将 string 或者 number 类型的输入值转换成 '数字 + “px” 格式的函数,如下代码所示:
function formatPX(size: unknown) {
if (typeof size === 'number') {
return `${
size}px`;
}
if (typeof size === 'string') {
return `${
parseInt(size) || 0}px`;
}
throw Error('仅支持转换 number 与 string');
}
console.log(formatPX(13)); // 13px
console.log(formatPX('13px')); // 13px
说明:在学习联合类型之前,我们可能免不了使用 any 或 unknown 类型来表示参数的类型(为了养成好习惯,推荐使用 unknown)。
通过这样的方式带来的问题是,在调用 formatPX 时,我们可以传递任意的值,并且可以通过静态类型检测(使用 any 亦如是),但是运行时还是会抛出一个错误,例如:
formatPX(true);
formatPX(null);
这显然不符合我们的预期,因为 size 应该是更明确的,即可能也只可能是 number 或 string 这两种可选类型的类型。
所幸有联合类型,我们可以使用一个更明确表示可能是 number 或 string 的联合类型来注解 size 参数,如下代码所示:
function formatPX(size: number | string) {
// ...
}
当然,我们可以组合任意个、任意类型来构造更满足我们诉求的类型。比如,我们希望给前边的示例再加一个 unit 参数表示可能单位,这个时候就可以声明一个字符串字面类型组成的联合类型,如下代码所示:
function formatPX(size: number | string, unit: 'px' | 'em' | 'rem' | '%' = 'px') {
if (typeof size === 'number') {
return `${
size}${
unit}`;
}
if (typeof size === 'string') {
return `${
parseInt(size) || 0}${
unit}`;
}
throw Error('仅支持转换 number 与 string');
}
console.log(formatPX(21, 'rem')); // 21rem
console.log(formatPX(77, '%')); // 77%
上述代码定义了 formatPX 函数的第二个参数 unit,它的类型是由 ‘px’、‘em’、‘rem’、‘%’ 字符串字面类型组成的类型集合。
我们也可以使用类型别名抽离上边的联合类型,然后再将其进一步地联合,如下代码所示:
type ModernUnit = 'vh' | 'vw';
type SimpleUnit = 'px' | 'em' | 'rem';
type UnitConcat = ModernUnit | SimpleUnit;
我们也可以把接口类型联合起来表示更复杂的结构,如下所示示例:
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();
Pet.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();
}
因为 Pet 的类型既可能是 Bird 也可能是 Fish,这就意味着在第 1 行可能会通过 Fish 类型获取 fly 属性,但 Fish 类型没有 fly 属性定义,所以会提示一个 ts(2339) 错误。
交叉类型
在 TypeScript 中,还存在一种类似逻辑与行为的类型——交叉类型(Intersection Type),它可以把多个类型合并成一个类型,合并后的类型将拥有所有成员类型的特性。
可以使用 & 操作符来声明交叉类型,如下代码所示:
type Test = string & number;
很显然,如果我们仅仅把原始类型、字面量类型、函数类型等原子类型合并成交叉类型,是没有任何用处的,因为任何类型都不能满足同时属于多种原子类型,比如既是 string 类型又是 number 类型。因此,在上述的代码中,类型别名 Test 的类型就是个 never。
合并接口类型
联合类型真正的用武之地就是将多个接口类型合并成一个类型,从而实现等同接口继承的效果,也就是所谓的合并接口类型,如下代码所示:
type IntersectionType = {
id: number; name: string; } & {
age: number };
const mixed: IntersectionType = {
id: 1,
name: 'Jae',
age: 23
}
在上述示例中,我们通过交叉类型,使得 IntersectionType 同时拥有了 id、name、age 所有属性,这里我们可以试着将合并接口类型理解为求并集。
如果合并的多个接口类型存在同名属性会是什么效果呢?
如果同名属性的类型不兼容,比如上面示例中两个接口类型同名的 name 属性类型一个是 number,另一个是 string,合并后,name 属性的类型就是 number 和 string 两个原子类型的交叉类型,即 never,如下代码所示:
type IntersectionType = {
id: number; name: string; } & {
age: number; name: number };
const mixed: IntersectionType = {
id: 1,
name: 'Jae', // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
age: 23
}
此时,我们赋予 mixed 任意类型的 name 属性值都会提示类型错误。而如果我们不设置 name 属性,又会提示一个缺少必选的 name 属性的错误。在这种情况下,就意味着上述代码中交叉出来的 IntersectionType 类型是一个无用类型。
如果同名属性的类型兼容,比如一个是 number,另一个是 number 的子类型、数字字面量类型,合并后 name 属性的类型就是两者中的子类型。
如下所示示例中 name 属性的类型就是数字字面量类型 2,因此,我们不能把任何非 2 之外的值赋予 name 属性。
type IntersectionType = {
id: number; name: 2; } & {
age: number; name: number };
const mixed: IntersectionType = {
id: 1,
name: 22, // ts(2322) 不能将类型“22”分配给类型“2”
age: 23
}
合并联合类型
另外,我们可以合并联合类型为一个交叉类型,这个交叉类型需要同时满足不同的联合类型限制,也就是提取了所有联合类型的相同类型成员。这里,我们也可以将合并联合类型理解为求交集。
在如下示例中,两个联合类型交叉出来的类型 IntersectionUnion 其实等价于 ‘em’ | ‘rem’,所以我们只能把 ‘em’ 或者 ‘rem’ 字符串赋值给 IntersectionUnion 类型的变量。
type UnionA = 'px' | 'em' | 'rem' | '%'
type UnionB = 'vh' | 'em' | 'rem' | 'pt'
type IntersectionUnion = UnionA & UnionB
const test1: IntersectionUnion = 'em'
const test2: IntersectionUnion = 'px' // ts(2322) 不能将类型“"px"”分配给类型“"em" | "rem"”
const test3: IntersectionUnion = 'pt' // ts(2322) 不能将类型“"pt"”分配给类型“"em" | "rem"”
既然是求交集,如果多个联合类型中没有相同的类型成员,交叉出来的类型自然就是 never 了,如下代码所示:
type UnionC = 'em' | 'rem';
type UnionD = 'px' | 'pt';
type IntersectionUnion2 = UnionC & UnionD;
const Test: IntersectionUnion2 = 'any' as any; // ts(2322) 不能赋予 'never' 类型
在上述示例中,因为 UnionC 和 UnionD 没有交集,交叉出来的类型 IntersectionUnion2就是 never,所以我们不能把任何类型的值赋予 IntersectionUnion2 类型的变量。
联合、交叉组合
在前面的示例中,我们把一些联合、交叉类型抽离成了类型别名,再把它作为原子类型进行进一步的联合、交叉。其实,联合、交叉类型本身就可以直接组合使用,这就涉及 |、& 操作符的优先级问题。实际上,联合、交叉运算符不仅在行为上表现一致,还在运算的优先级和 JavaScript 的逻辑或 ||、逻辑与 && 运算符上表现一致 。
联合操作符 | 的优先级低于交叉操作符 &,同样,我们可以通过使用小括弧 () 来调整操作符的优先级。
type ITest1 = {
id: number; } & {
name: string; } | {
id: string; } & {
name: number; }; // 交叉操作符优先级高于联合操作符
type ITest2 = ('px' | 'em' | 'rem' | '%') | ('vh' | 'em' | 'rem' | 'pt'); // 调整优先级
类型缩减
如果将 string 原始类型和“string字面量类型”组合成联合类型会是什么效果?效果就是类型缩减成 string 了,如下所示:
type TStr = 'string' | string; // 类型是 string
type TNum = 2 | number; // 类型是 number
type TBoolean = true | boolean; // 类型是 boolean
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;
});
这里因为 ‘1’ 是 ‘1’ | ‘2’ 的子集,所以 age 的属性变成 ‘1’ | ‘2’。
利用这个特性,来解决一个问题:如何定义如下所示 age 属性是数字类型,而其他不确定的属性是字符串类型的数据结构的对象?
{
num: 1, // 数字类型
str: 'str', // 其他不确定的属性都是字符串类型
...
}
我们肯定要用到两个接口的联合类型及类型缩减,这个问题的核心在于找到一个既是 number 的子类型,这样 age 类型缩减之后的类型就是 number;同时也是 string 的子类型,这样才能满足属性和 string 索引类型的约束关系。
哪个类型满足这个条件呢?它就是 never
never 有一个特性是它是所有类型的子类型,自然也是 number 和 string 的子类型,所以答案如下代码所示:
type UnionInterce =
| {
age: number;
}
| ({
age: never;
[key: string]: string;
});
const test: UnionInterce = {
age: 2,
string: 'string'
}
在上述代码中,先是定义了 number 类型的 age 属性,然后定义了 never 类型的 age 属性,等价于 age 属性的类型是由 number 和 never 类型组成的联合类型,所以我们可以把 number 类型的值(比如说数字字面量 1)赋予 age 属性;但是不能把其他任何类型的值(比如说字符串字面量 ‘string’ )赋予 age。
同时,后面定义的接口类型中,还额外定义了 string 类型的字符串索引签名。因为 never 同时又是 string 类型的子类型,所以 age 属性的类型和字符串索引签名类型不冲突。如代码中所示,我们可以把一个 age 属性是 2、string 属性是 ‘string’ 的对象字面量赋值给 UnionInterce 类型的变量 test。
边栏推荐
- Golang --- comparison operation of various types of variables
- OpenGL 01 - créer une fenêtre
- P5354 [ynoi2017] yoyo OJ (tree section and bit operation)
- Wuqi_ New progress in vision language navigation: pre training and sim2real
- Status mode simulates elevator operation
- Devin round smart screen comes into the market
- Give an example to illustrate the cell and num of lstmcell in TF_ What does unit mean
- oracle网吧设计用程序实现插入更新删除的问题
- (7)属性绑定
- Test website construction + penetration + audit Part II penetration test
猜你喜欢

How to calculate the rarity of NFT?

Golang - - paquet d'exécution simultané

My creation anniversary

Iscc-2022-reverse-mobile- part WP

Golang --- comparison operation of various types of variables

Internet behavior networking

渲染管线----通俗易懂向面试官介绍

Number precision-- use / instance

Give an example to illustrate the cell and num of lstmcell in TF_ What does unit mean

数据库连接问题,换版本后无法获取连接
随机推荐
Win10 installing appium environment
View local public IP
MySQL queries which table in the database has the most fields
Test website construction + penetration + audit Part II penetration test
[004] [ESP32开发笔记] 音频开发框架ADF环境搭建——基于ESP-IDF
MySQL: common statistics, grouping statistics and time format conversion
(2) v-cloak指令
Introduction of ravless speech emotion classification data set
lua 字符串
Win10安装appium环境
Installation and performance test of API gateway Apache APIs IX on AWS graviton3
Format interchange among Yolo, coco and VOC datasets
Give an example to illustrate the cell and num of lstmcell in TF_ What does unit mean
MySQL中的多表查询
MMdet修改检测框字体大小、位置、颜色、填充框
(8) Style binding
Types, advantages and disadvantages of MySQL indexes
Rigidbody2d SweepTest Rigidbody2D. Cast but for rotation rotation
Multi table query in MySQL
openGL_01-創建窗口