TS进阶类型

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种

TypeScript 的类型系统允许我们使用多种运算符,从现有类型中构建新类型

使用第一种组合类型的方法:联合类型(Union Type)

  • 联合类型是由两个或者多个其他类型组成的类型
  • 表示可以是这些类型中的任何一个值
  • 联合类型中的每一个类型被称之为联合成员(union’s members)

需求 1: 定义一个函数得到一个数字或字符串值的字符串形式值

1
2
3
function toString2(x: number | string): string {
return x.toString();
}

需求 2: 定义一个函数得到一个数字或字符串值的长度

1
2
3
4
5
6
7
8
9
10
function getLength(x: number | string) {
// return x.length // error

if (x.length) {
// error
return x.length;
} else {
return x.toString().length;
}
}

处理多种类型问题

  • 需要使用缩小(narrow)联合
  • TypeScript 可以根据我们缩小的代码结构,推断出更加具体的类型
1
2
3
4
5
6
7
function getLength(x: number | string) {
if (typeof x === "string") {
return x.length;
} else {
return x.toString().length;
}
}

类型断言

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”

类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构

它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查

类型断言有两种形式:其一是“尖括号”语法;另一个为 as 语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 类型断言(Type Assertion): 可以用来手动指定一个值的类型
* 方式一: <类型>值
* 方式二: 值 as 类型 tsx中只能用这种方式
**/

/* 需求: 定义一个函数得到一个字符串或者数值数据的长度 */
function getLength(x: number | string) {
if ((<string>x).length) {
return (x as string).length;
} else {
return x.toString().length;
}
}
console.log(getLength("abcd"), getLength(1234));
1
2
3
4
5
6
7
8
9
// 类型断言的规则: 断言只能断言成更加具体的类型, 或者 不太具体(any/unknown) 类型
const age: number = 18;
// 错误的做法
// const age2 = age as string // error

// TS类型检测来说是正确的, 但是这个代码本身不太正确
const age3 = age as any;
const age4 = age3 as string;
console.log(age4.split(" "));

类型推断

类型推断: TS 会在没有明确的指定类型的时候推测出一个类型,有下面 2 种情况:

  • 定义变量时赋值了, 推断为对应的类型
  • 定义变量时没有赋值, 推断为 any 类型
1
2
3
4
5
6
7
8
/* 定义变量时赋值了, 推断为对应的类型 */
let b9 = 123; // number
// b9 = 'abc' // error

/* 定义变量时没有赋值, 推断为any类型 */
let b10; // any类型
b10 = 123;
b10 = "abc";

类型别名

以前通过在类型注解中编写 对象类型 和 联合类型,如果想要多次在其他地方使用时,就要编写多次,就可以给对象类型起一个别名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 类型别名: type
type MyNumber = number;
const age: MyNumber = 18;

// 给ID的类型起一个别名
type IDType = number | string;

function printID(id: IDType) {
console.log(id);
}

// 打印坐标
type PointType = { x: number; y: number; z?: number };
function printCoordinate(point: PointType) {
console.log(point.x, point.y, point.z);
}

接口声明

可以通过interface可以用来声明一个对象类型,此处为了和type声明作对比,简要说明基本用法,详细见接口章节

1
2
3
4
5
6
7
8
9
10
11
12
13
type PointType = {
x: number;
y: number;
z?: number;
};

// 接口: interface
// 声明的方式
interface PointType2 {
x: number;
y: number;
z?: number;
}

类型与接口对比

两者区别

  • 类型别名和接口非常相似,在定义对象类型时,大部分时候,可以任意选择使用
  • 接口的几乎所有特性都可以在 type 中使用(后续会有interface的很多特性)
  • 如果是定义非对象类型,通常推荐使用type,比如DirectionAlignment、一些Function
  • interface 可以重复的对某个接口来定义属性和方法,而type定义的是别名,别名是不能重复的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 1.区别一: type类型使用范围更广, 接口类型只能用来声明对象
type MyNumber = number;
type IDType = number | string;

// 2.区别二: 在声明对象时, interface可以多次声明
// 2.1. type不允许两个相同名称的别名同时存在
type PointType1 = {
x: number;
y: number;
};

// type PointType1 = { // error
// z?: number
// }

// 2.2. interface可以多次声明同一个接口名称
interface PointType2 {
x: number;
y: number;
}

interface PointType2 {
z: number;
}

const point: PointType2 = {
x: 100,
y: 200,
z: 300
};

// 3.interface支持继承的
interface IPerson {
name: string;
age: number;
}

interface IKun extends IPerson {
kouhao: string;
}

const ikun1: IKun = {
kouhao: "你干嘛, 哎呦",
name: "kobe",
age: 30
};

// 4.interface可以被类实现(详见TS面向对象)
class Person implements IPerson {}

// 总结: 如果是非对象类型的定义使用type, 如果是对象类型的声明那么使用interface

交叉类型

另外一种类型合并,就是交叉类型(Intersection Types)

  • 交叉类似表示需要满足多个类型的条件
  • 交叉类型使用 & 符号

交叉类型栗子:

  • 一个交叉类型:表达的含义是numberstring要同时满足
  • 但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以MyType其实是一个never类型
1
type MyType = number & string; // MyType: never

在开发中,进行交叉时,通常是对对象类型进行交叉的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 交叉类型: 两种(多种)类型要同时满足
type NewType = number & string; // 没有意义

interface IKun {
name: string;
age: number;
}

interface ICoder {
name: string;
coding: () => void;
}

type InfoType = IKun & ICoder;

const info: InfoType = {
name: "why",
age: 18,
coding: function () {
console.log("coding");
}
};

非空类型断言

非空断言使用的是 ! ,表示可以确定某个标识符是有值的,跳过 ts 在编译阶段对它的检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 定义接口
interface IPerson {
name: string;
age: number;
friend?: {
name: string;
};
}

const info: IPerson = {
name: "why",
age: 18
};

// 访问属性: 可选链: ?.
console.log(info.friend?.name);

// 属性赋值:
// 解决方案一: 类型缩小
if (info.friend) {
info.friend.name = "kobe";
}

// 解决方案二: 非空类型断言(有点危险, 只有确保friend一定有值的情况, 才能使用)
info.friend!.name = "james";

字面量类型

1
2
3
4
5
6
7
// 1.字面量类型的基本上
const name: "why" = "why";
let age: 18 = 18;

// 2.将多个字面量类型联合起来 |
type Direction = "left" | "right" | "up" | "down";
const d1: Direction = "left";

字面量推理

栗子中:对象在进行字面量推理的时候,info 其实是一个 {url: string, method: string},所以不能将一个 string 赋值给一个 字面量 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 栗子: 封装请求方法
type MethodType = "get" | "post";
function request(url: string, method: MethodType) {}

request("http://www.aaa.com", "post");

// TS细节
const info = {
url: "xxxx",
method: "post"
};
// 下面的做法是错误: info.method获取的是string类型
request(info.url, info.method);

// 解决方案一: info.method进行类型断言
request(info.url, info.method as "post");

// 解决方案二: 直接让info对象类型是一个字面量类型
const info2: { url: string; method: "post" } = {
url: "xxxx",
method: "post"
};

// 解决方案三: 将info断言为字面量类型
const info3 = {
url: "xxxx",
method: "post"
} as const;
// xxx 本身就是一个string
request(info3.url, info3.method);

类型缩小

类型缩小的含义

  • 类型缩小的英文是 Type Narrowing(也有人翻译成类型收窄)
  • 可以通过类似于 typeof padding === "number" 的判断语句,来改变 TypeScript 的执行路径
  • 在给定的执行路径中,可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing )
  • 而编写的 typeof padding === "number" 可以称之为 类型保护(type guards)

常见的类型保护有如下几种:

  • typeof
  • 平等缩小(比如===、!==)
  • instanceof
  • in
  • 等等…

typeof

在 TypeScript 中,检查返回的值typeof是一种类型保护,TypeScript 对如何typeof操作不同的值进行编码

1
2
3
4
5
6
7
8
// 1.typeof: 使用的最多
function printID(id: number | string) {
if (typeof id === "string") {
console.log(id.length, id.split(" "));
} else {
console.log(id);
}
}

平等缩小

可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != )

1
2
3
4
5
6
7
8
9
10
11
12
13
// 2.===/!==: 方向的类型判断
type Direction = "left" | "right" | "up" | "down";
function switchDirection(direction: Direction) {
if (direction === "left") {
console.log("左:", "角色向左移动");
} else if (direction === "right") {
console.log("右:", "角色向右移动");
} else if (direction === "up") {
console.log("上:", "角色向上移动");
} else if (direction === "down") {
console.log("下:", "角色向下移动");
}
}

instanceof

JavaScript 有一个运算符instanceof来检查一个值是否是另一个值的“实例”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 3. instanceof: 传入一个日期, 打印日期
function printDate(date: string | Date) {
if (date instanceof Date) {
console.log(date.getTime());
} else {
console.log(date);
}

// if (typeof date === "string") {
// console.log(date)
// } else {
// console.log(date.getTime())
// }
}

in 操作符

Javascript 有一个运算符,用于确定对象是否具有带名称的属性:in运算符

  • 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 4.in: 判断是否有某一个属性
interface ISwim {
swim: () => void;
}

interface IRun {
run: () => void;
}

function move(animal: ISwim | IRun) {
if ("swim" in animal) {
animal.swim();
} else if ("run" in animal) {
animal.run();
}
}

const fish: ISwim = {
swim: function () {}
};

const dog: IRun = {
run: function () {}
};

move(fish);
move(dog);