「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
有了前面所学内容的铺垫,本章我们学习的高级类型中,有一些是之前所学知识的结合,有一些是在之前内容上提升的新东西。
学习高级类型可以满足我们工作中更多场景的需求。
1、交叉类型
交叉类型就是取多个类型的并集,使用 & 符号定义。
const mergeFunc = <T, U>(arg1: T, arr2: U): T & U => {
let res = {} as T & U; // 使用类型断言来告诉TS这里是(T和U)的交叉类型
res = Object.assign(arg1, arr2);
return res;
};
mergeFunc({ a: 'a' }, { b: 'b' });
2、联合类型
例如:type1 | type2 | type3,这里是三种类型组成的联合类型,用到联合类型的地方,只要类型是这三种类型其中的一种就可以了。
来看下面的例子:
const getLength = (content: string | number): number => {
if(typeof content === 'string') {
return content.length;
} else {
return content.toString().length;
}
}
3、类型保护
我们知道 TS 在编译阶段就会为我们检查到一些错误,但是有一些值是在代码运行起来之后才能确定的。
const valueList = [123, 'abc'];
// 这个函数会随机从 valueList 取值
const getRandomValue = () => {
const number = Math.random() * 10;
if(number < 5) return valueList[0];
return valueList[1];
};
const item = getRandomValue();
此时 item 在编译阶段是无法知道它的类型的。所以,我们有时候就需要先判断它的类型。
if(item.length) {
console.log(item.length);
} else {
console.log(item.toFixed());
}
上述代码这段逻辑在 JS 中是没有问题的,但是在 TS 中就会报错:类型“string | number”上不存在属性“length”。
类型断言
if((item as string).length) {
console.log((item as string).length);
} else {
console.log((item as number).toFixed());
}
但是类型断言有个缺点:如果多个地方使用到 item,每个使用到的地方都需要类型断言。
typeof 类型保护
我们先来自定义类型保护(它是一个方法)
function isString(value: number | string): value is string {
return typeof value === 'string';
}
返回类型是 value is string,表示判断 是否是 string 类型。
现在我们来使用类型保护
if(isString(item)) {
console.log(item.length);
} else {
console.log(item.toFixed());
}
可以看到,这里我们多定义了一个函数 isString,实际上定义函数的方式多用在复杂的类型保护。类型保护如果像这里一样,比较简单的话,建议直接使用 typeof:(实际上就是把函数拆出来)
if(typeof value === 'string') {
console.log(item.length);
} else {
console.log(item.toFixed());
}
在 TS 中,typeof的类型保护,只能用等号或者不等来比较,并且只能比较简单类型:string、number、boolean、symbol
注:typeof在ts中可以判断很多种类型,但是如果用作类型保护的话,只能是这四种:string、number、boolean、symbol
instanceof 类型保护
instanceof 用来判断一个实例是否是某个构造函数(或者某个class)创建的。
在 TS 中 instanceof 同样具有类型保护的效果。
class CreatedByClass1 {
public age = 18;
constructor() {}
}
class CreatedByClass2 {
public name = 'dylan';
constructor() {}
}
function getRandomItem() {
return Math.random() < 0.5 ? new CreatedByClass1() : new CreatedByClass2();
}
const item = getRandomItem();
if(item instanceof CreatedByClass1) {
console.log(item.age);
} else {
console.log(item.name);
}
4、null 和 undefined
注意需要将tslint的 strictNullChecks 设置为 false。如果设置为 true的话,可选参数会被自动加上 undefined 变为联合类型。
// 这里 y 会发现它是一个 number | undefined 联合类型
const sumFunc = (x: number, y?: number) => {
return x + (y || 0)
};
在 TS 中 string | undefined、string | null、string | undefined | null 这三个是三种完全不同的类型
5、类型断言
在有些情况下,编译器是无法在我们声明一些变量前知道它的值是否为 null,所以我们需要断言,主动指名变量的值不为 null
function getSplicedStr(num: number | null): string {
function getRes(prefix: string) {
// 当我们设置 strictNullChecks 为 true 时,这里 num 会报错:对象可能为 "null"。
return prefix + num.toFixed().toString();
}
num = num || 0.1;
return getRes('dylan-');
}
我们可以看到,num 此时类型为:(parameter) num: number | null
在这个例子中,因为有嵌套函数,编译器是无法去除嵌套函数的 null 的,所以这里可以使用类型断言 !
function getSplicedStr(num: number | null): string {
function getRes(prefix: string) {
// 使用 ! 进行类型断言,主动告诉编译器这里一定有值
return prefix + num!.toFixed().toString();
}
num = num || 0.1;
return getRes('dylan-');
}
6、类型别名
用 type 关键字来定义类型别名。不是创建了一个新的对象,而是通过引用来使用这个对象。
type TypeString = string;
let str: TypeString;
类型别名也可以使用泛型:
type PositionType<T> = { x: T, y: T }
const position1: PositionType<number> = {
x: 1,
y: -1,
}
const position2: PositionType<string> = {
x: 'left',
y: 'top',
}
使用类型别名的时候,也可以在属性中引用自己:
type Childs<T> = {
current: T,
child?: Childs<T>, // 树状结构
};
类型别名只是为其他类型起了个新的名字来引用这个类型,所以当它为接口起别名时,不能使用
extends和implements。因为它只是个名字,不是一个真正的接口。
type Alias = {
num: number
}
interface InterFace {
num: number
}
let _alias: Alias = {
num: 123
}
let _interface: InterFace = {
num: 321
}
可以看到,类型别名和接口都可以定义一个只包含一个参数为 num的值。并且他们的类型是兼容的
_alias = _interface; // OK
那么,我们什么时候用类型别名,什么时候用接口呢?
- 接口:当你定义的类型,需要用于扩展(extends、implements)的时候
- 类型别名:当无法通过接口并且需要使用联合类型或元组类型的时候
7、字面量类型
字面量类型不适合放在基础类型中来讲,因为字符串字面量类型其实和字符串类型并不一样。
字面量类型包含两种,分别是:数字字面量、字符串字面量。
type Name = 'Dylan'; // 字符串字面量类型
const name: Name = 'Dylan1'; // Error:不能将类型“"Dylan1"”分配给类型“"Dylan"”。
使用联合类型来定义多个字符串字面量
type Direction = 'north' | 'east' | 'south' | 'west';
function getDirection(direction: Direction) {
return direction;
}
getDirection("east"); // 这里传入参数就会有提示,只能选这四个
数字字面量类型
type Age = 18;
interface InfoInterface {
name: string;
age: Age
}
const _info: InfoInterface = {
name: 'dylan',
age: 18, // 这里的 age 只能填 18
}
8、可辨识联合
可辨识联合要求具有两个要素:
- 具有普通的单例类型属性(作为辨识的特征)
- 一个类型别名包含了哪些类型的联合(把几个类型封装成联合类型,并且起个别名)
interface Square {
kind: 'square';
size: number;
}
interface Rectangle {
kind: 'reactangle';
height: number;
width: number;
}
interface Circle {
kind: 'circle';
radius: number;
}
上面代码我们定义了三个接口,他们都有同一个特征 kind,值都不一样
接下来我们再定义一个联合类型:
type Shape = Square | Rectangle | Circle;
再来,我们定义一个函数:
function assertNever(value: never): never {
throw new Error('Unexpected object: ' + value);
}
function getArea(s: Shape): number {
switch(s.kind) {
case "square": return s.size * s.size;
case "reactangle": return s.height * s.width;
case "circle": return Math.PI * s.radius ** 2;
default: return assertNever(s); // !完整性检查
}
}