typescript学习:第二部分

第一部分跳转链接

经过第一部分的学习,我们已经了解到了typescript的基本用法,同时对于其在代码健壮性,以及编译过程中即出现错误提示的有点有了更一步的了解,接下来,我们将进一步学习新的知识,体会typescript更加高级的用法。

类型断言

在学习类型断言之前,需要先了解断言的具体概念,针对node语言里面也有相关断言的概念,有以下的问题:
1.断言是一个广泛的概念吗?
2.node断言与typescript类型断言分别都是什么?

首先可以回答第一个问题: 是,那么第二个问题就没有必要再纠结了。

菜鸟教程–asserts断言的用法.

可以这么说:断言assert 是仅在Debug 版本起作用的宏,它用于检查”不应该”发生的情况。

需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

以下是使用断言的几个原则:
(1)使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
(2)使用断言对函数的参数进行确认。
(3)在编写函数时,要进行反复的考查,并且自问:”我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
(4)一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果”不可能发生”的事情的确发生了,则要使用断言进行报警。

类型断言的用法

前文提到,对于断言的使用是需要慎重的。这节我们将学习断言(assert)的具体用法:
方法1: 值 as 类型
方法2: <类型>值

由于第二种方法在使用过程中可能会产生歧义,因为

在 tsx 语法(React 的 jsx 语法的 ts 版)中必须使用前者,即 值 as 类型。形如 的语法在 tsx 中表示的是一个 ReactNode,在 ts 中除了表示类型断言之外,也可能是表示一个泛型。故建议大家在使用类型断言时,统一使用 值 as 类型 这样的语法,本书中也会贯彻这一思想。

类型断言的用途

将一个联合类型推断为其中一个类型

第一部分提到过,在不确定一个具有联合类型的变量具体是哪一种类型时,只能访问此联合类型的所有类型中共有的属性和方法:

1
2
3
4
5
6
7
8
9
10
11
12
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

function getName(animal: Cat | Fish) {
return animal.name;
}

而当我们确实需要在不确定类型的时候就访问其中一个类型所特有的属性或方法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}

// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.

上面的例子中,获取 animal.swim 的时候会报错。

此时可以使用类型断言,将 animal 断言成 Fish:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}

function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}

需要注意的是,类型断言只能够「欺骗」TypeScript 编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}

const tom: Cat = {
name: 'Tom',
run() { console.log('run')}
};
swim(tom);
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.

错误预览

首先理解一下函数 siwm函数传递参数animal,然后参数调用该类型的属性和方法。不太理解的地方可以查看上方的错误预览。

原因是 (animal as Fish).swim() 这段代码隐藏了 animal 可能为 Cat 的情况,将 animal 直接断言为 Fish 了,而 TypeScript 编译器信任了我们的断言,故在调用 swim() 时没有编译错误。

可是 swim 函数接受的参数是 Cat | Fish,一旦传入的参数是 Cat 类型的变量,由于 Cat 上没有 swim 方法,就会导致运行时错误了。

总之,使用类型断言时一定要格外小心,尽量避免断言后调用方法或引用深层属性,以减少不必要的运行时错误。

将一个父类断言为更加具体的子类

当类之间有继承关系时,类型断言也是常见的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ApiError extends Error {
code: number = 0;
}

class HttpError extends Error {
statusCode: number = 200;
}

function isApiError(error: Error) {
if(typeof (error as ApiError).code === 'number') {
return true;
}

return false;
}

isApiError函数中,它用于实现传入的错误是不是ApiError类型。

有的情况下 ApiError 和 HttpError 不是一个真正的类,而只是一个 TypeScript 的接口(interface),接口是一个类型,不是一个真正的值,它在编译结果中会被删除,当然就无法使用 instanceof 来做运行时判断了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}

function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}

// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.

需要学习巩固一下: 类与继承

将任何一个类型断言为any类型

在 any 类型的变量上,访问任何属性都是允许的。

需要注意的是,将一个变量断言为 any 可以说是解决 TypeScript 中类型问题的最后一个手段。

它极有可能掩盖了真正的类型错误,所以如果不是非常确定,就不要使用 as any。

上面的例子中,我们也可以通过[扩展 window 的类型(TODO)][]解决这个错误,不过如果只是临时的增加 foo 属性,as any 会更加方便。

总之,一方面不能滥用 as any,另一方面也不要完全否定它的作用,我们需要在类型的严格性和开发的便利性之间掌握平衡(这也是 TypeScript 的设计理念之一),才能发挥出 TypeScript 最大的价值。

将 any 断言为一个具体的类型

在日常的开发中,我们不可避免的需要处理 any 类型的变量,它们可能是由于第三方库未能定义好自己的类型,也有可能是历史遗留的或其他人编写的烂代码,还可能是受到 TypeScript 类型系统的限制而无法精确定义类型的场景。

遇到 any 类型的变量时,我们可以选择无视它,任由它滋生更多的 any。

我们也可以选择改进它,通过类型断言及时的把 any 断言为精确的类型,亡羊补牢,使我们的代码向着高可维护性的目标发展。

1
2
3
4
5
6
7
8
9
10
11
function getCacheData(key: string): any {
return (window as any).cache[key];
}

interface Cat {
name: string;
run(): void;
}

const tom = getCacheData('tom') as Cat;
tom.run();

上面的例子,这个函数调用完后的类型为any,我们可以采用接口的方式将它断言为具体的cat类型,后续对参数的访问就会有相应的代码补全了,能够提高代码的可维护性。

后续参考链接:类型断言、类型转换、类型声明、泛型

泛型(generics)

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。