TypeScript - 高级类型
# 1. 交叉类型
交叉类型(Intersection Types)是将多个类型合并为一个类型。这种方式可以将多个类型的成员合并到一个类型中,使得新类型同时具备所有类型的特性。交叉类型通常用于混入(mixins)或其他不符合传统面向对象模型的场景。
示例:
function extend<T, U>(first: T, second: U): T & U {
let result = {} as T & U;
for (let id in first) {
result[id] = first[id] as any;
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
result[id] = second[id] as any;
}
}
return result;
}
class Person {
constructor(public name: string) {}
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
console.log('Logging...');
}
}
let jim = extend(new Person('Jim'), new ConsoleLogger());
let n = jim.name;
jim.log();
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
在这个例子中,extend
函数将 Person
和 ConsoleLogger
合并成一个新的对象,该对象同时具有 Person
和 Loggable
的成员。这说明 jim
变量既可以访问 name
属性,也可以调用 log
方法。
# 2. 联合类型
联合类型(Union Types)与交叉类型类似,但其使用方式不同。联合类型表示一个值可以是几种类型之一。在需要一个值可以是多种类型之一的情况下,使用联合类型是很有用的。
示例:
function padLeft(value: string, padding: string | number) {
if (typeof padding === 'number') {
return Array(padding + 1).join(' ') + value;
}
if (typeof padding === 'string') {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
let indentedString = padLeft('Hello world', 4); // 返回 " Hello world"
2
3
4
5
6
7
8
9
10
11
在这个例子中,padLeft
函数的 padding
参数可以是 string
或 number
类型。通过使用联合类型,我们可以限制 padding
只能是 string
或 number
,避免传入其他类型导致的错误。
联合类型表示一个值可以是几种类型之一。我们使用竖线(|
)分隔每个类型,例如 number | string
表示一个值可以是 number
或 string
。
当一个值是联合类型时,我们只能访问这个联合类型中所有类型共有的成员。
示例:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getSmallPet(): Fish | Bird {
// 实现省略
return {} as Fish; // 示例返回一个 Fish 类型
}
let pet = getSmallPet();
pet.layEggs(); // 正常
pet.swim(); // 错误:类型 'Bird | Fish' 上不存在属性 'swim'。
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个例子中,getSmallPet
函数返回一个 Fish
或 Bird
类型的对象。pet
变量是 Fish | Bird
类型,我们可以调用 layEggs
方法,因为它是 Fish
和 Bird
的公共成员。但不能调用 swim
方法,因为 Bird
类型没有 swim
方法。
联合类型允许我们根据类型的不同来编写更加灵活的代码,同时保持类型的安全性。
# 3. 类型保护
联合类型适合用于那些值可以是不同类型的场景。但有时我们希望确切地了解一个值是 Fish
还是 Bird
。在 JavaScript 中,我们可以通过检查对象成员的存在来区分这两种可能的值。在 TypeScript 中,由于只能访问联合类型中共有的成员,因此我们需要使用类型保护来确保特定的类型。
let pet = getSmallPet();
// 每一个成员访问都会报错,因为 TypeScript 无法确定 pet 的具体类型
if (pet.swim) {
pet.swim();
} else if (pet.fly) {
pet.fly();
}
2
3
4
5
6
7
8
为了让这段代码正常工作,我们需要使用类型断言,告诉 TypeScript 编辑器该对象是某种特定的类型:
let pet = getSmallPet();
// 使用类型断言,将 pet 视为 Fish 类型
if ((pet as Fish).swim) {
(pet as Fish).swim();
} else {
// 使用类型断言,将 pet 视为 Bird 类型
(pet as Bird).fly();
}
2
3
4
5
6
7
8
9
# 用户自定义的类型保护
在上面的代码中,我们需要多次使用类型断言。如果我们能够在检查过类型后,在每个分支中清楚地知道 pet
的类型,那会更加简洁。TypeScript 的类型保护机制让这成为可能。类型保护是一种运行时检查,以确保某个范围内的变量属于特定类型。定义一个类型保护,我们可以简单地定义一个返回值为类型谓词的函数:
// 定义类型保护函数,判断 pet 是否为 Fish 类型
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
2
3
4
在这个例子中,pet is Fish
就是一个类型谓词。类型谓词的形式为 parameterName is Type
,其中 parameterName
必须是当前函数签名中的一个参数名。
每当使用一个变量调用 isFish
时,TypeScript 会将变量缩小为那个具体的类型。
if (isFish(pet)) {
// 在此分支中,pet 被缩小为 Fish 类型
pet.swim();
} else {
// 在此分支中,pet 被缩小为 Bird 类型
pet.fly();
}
2
3
4
5
6
7
TypeScript 不仅知道在 if
分支中 pet
是 Fish
类型,它还知道在 else
分支中 pet
一定不是 Fish
类型,而是 Bird
类型。
# typeof 类型保护
现在我们回头看看如何使用联合类型来重写 padLeft
函数。我们可以像下面这样利用类型断言:
// 定义类型保护函数,判断 x 是否为 number 类型
function isNumber(x: any): x is number {
return typeof x === 'number';
}
// 定义类型保护函数,判断 x 是否为 string 类型
function isString(x: any): x is string {
return typeof x === 'string';
}
// 使用类型保护函数来实现类型检查
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(' ') + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
然而,我们不需要将 typeof x === 'number'
抽象成一个函数,因为 TypeScript 可以将它识别为类型保护。这意味着我们可以直接在代码中检查类型:
function padLeft(value: string, padding: string | number) {
if (typeof padding === 'number') {
// padding 是 number 类型
return Array(padding + 1).join(' ') + value;
}
if (typeof padding === 'string') {
// padding 是 string 类型
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
2
3
4
5
6
7
8
9
10
11
这些 typeof
类型保护仅限于两种形式:typeof v === "typename"
和 typeof v !== "typename"
。其中 typename
必须是 "number"
,"string"
,"boolean"
或 "symbol"
。虽然 TypeScript 不会阻止你与其他字符串进行比较,但它不会将这些表达式识别为类型保护。
# instanceof 类型保护
如果你已经了解了 typeof
类型保护,并且对 JavaScript 中的 instanceof
操作符熟悉,你可能已经猜到我们要讨论的内容。
instanceof
类型保护是通过构造函数来缩小类型的一种方式。我们对之前的例子做一个小小的改动:
// 定义 Bird 类
class Bird {
fly() {
console.log('bird fly');
}
layEggs() {
console.log('bird lay eggs');
}
}
// 定义 Fish 类
class Fish {
swim() {
console.log('fish swim');
}
layEggs() {
console.log('fish lay eggs');
}
}
// 随机返回 Bird 或 Fish 的实例
function getRandomPet() {
return Math.random() > 0.5 ? new Bird() : new Fish();
}
let pet = getRandomPet();
// 使用 instanceof 检查 pet 是否为 Bird 类型
if (pet instanceof Bird) {
pet.fly();
}
// 使用 instanceof 检查 pet 是否为 Fish 类型
if (pet instanceof Fish) {
pet.swim();
}
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
instanceof
用于检查自定义类,而 typeof
用于检查内置基本类型。通过使用这些类型保护机制,我们可以在代码中更安全地处理联合类型。
Sure! Here is the revised content with detailed comments in the code blocks:
# 4. 可以为 null 的类型
TypeScript 具有两种特殊的类型,null
和 undefined
,它们分别具有值 null
和 undefined
。我们在 TypeScript - 基础类型 中已经做过简要说明。默认情况下,类型检查器认为 null
与 undefined
可以赋值给任何类型。null
与 undefined
是所有其它类型的一个有效值。这也意味着,即使你想阻止将它们赋值给其它类型也不行。null
的发明者,Tony Hoare,称它为 价值亿万美金的错误 (opens new window)。
使用 --strictNullChecks
标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null
或 undefined
。你可以使用联合类型明确地包含它们:
let s: string = 'foo'; // 定义一个字符串类型的变量 s
s = null; // 错误, 'null' 不能赋值给 'string'
let sn: string | null = 'bar'; // 定义一个可以为字符串或 null 的变量 sn
sn = null; // 可以
sn = undefined; // 错误, 'undefined' 不能赋值给 'string | null'
2
3
4
5
6
注意,按照 JavaScript 的语义,TypeScript 会把 null
和 undefined
区别对待。string | null
,string | undefined
和 string | undefined | null
是不同的类型。
# 可选参数和可选属性
使用了 --strictNullChecks
,可选参数会被自动地加上 | undefined
:
function f(x: number, y?: number) {
// y 是一个可选参数,自动被加上 | undefined
return x + (y || 0);
}
f(1, 2); // 正常调用,y 被赋值为 2
f(1); // 正常调用,y 未赋值,默认为 undefined
f(1, undefined); // 正常调用,y 被显式赋值为 undefined
f(1, null); // 错误, 'null' 不能赋值给 'number | undefined'
2
3
4
5
6
7
8
可选属性也会有同样的处理:
class C {
a: number; // 必须初始化的属性
b?: number; // 可选属性,自动被加上 | undefined
}
let c = new C();
c.a = 12; // 正常赋值
c.a = undefined; // 错误, 'undefined' 不能赋值给 'number'
c.b = 13; // 正常赋值
c.b = undefined; // 可以赋值为 undefined
c.b = null; // 错误, 'null' 不能赋值给 'number | undefined'
2
3
4
5
6
7
8
9
10
# 类型保护和类型断言
由于可以为 null
的类型可以和其它类型定义为联合类型,你需要使用类型保护来去除 null
。幸运的是,这与在 JavaScript
里写的代码一致:
function f(sn: string | null): string {
if (sn === null) {
// 如果 sn 为 null,返回默认字符串
return 'default';
} else {
// 如果 sn 不为 null,返回 sn
return sn;
}
}
2
3
4
5
6
7
8
9
这里显式地去除了 null
,你也可以使用短路运算符:
function f(sn: string | null): string {
// 如果 sn 为 null,返回 'default',否则返回 sn
return sn || 'default';
}
2
3
4
如果编译器不能够去除 null
或 undefined
,你可以使用类型断言手动去除。语法是添加 !
后缀:identifier!
从 identifier
的类型里去除了 null
和 undefined
。
!
的作用就是告诉 TypeScript 编辑器:该属性一定存在值,不可能是 null 或者 undefined。
function broken(name: string | null): string {
function postfix(epithet: string) {
// 这里编译器报错,'name' 可能为 null
return name.charAt(0) + '. the ' + epithet;
}
name = name || 'Bob'; // 如果 name 为 null,赋值为 'Bob'
return postfix('great');
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
// 使用 ! 去除 name 的 null 类型
return name!.charAt(0) + '. the ' + epithet;
}
name = name || 'Bob'; // 如果 name 为 null,赋值为 'Bob'
return postfix('great');
}
broken(null); // 调用 broken 函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
本例使用了嵌套函数,因为编译器无法去除嵌套函数的 null
(除非是立即调用的函数表达式)。因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。如果无法知道函数在哪里被调用,就无法知道调用时 name
的类型。
# 5. 字符串字面量类型
字符串字面量类型允许你指定字符串必须具有的确切值。在实际应用中,字符串字面量类型可以与联合类型和类型保护很好地配合。通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = 'ease-in' | 'ease-out' | 'ease-in-out'; // 定义字符串字面量类型
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === 'ease-in') {
// ...
} else if (easing === 'ease-out') {
// ...
} else if (easing === 'ease-in-out') {
// ...
} else {
// error! 不能传入 null 或者 undefined.
}
}
}
let button = new UIElement();
button.animate(0, 0, 'ease-in'); // 正确调用,传入了允许的字符串字面量
button.animate(0, 0, 'uneasy'); // 错误,传入了不允许的字符串字面量
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
你只能从三种允许的字符中选择其一来作为参数传递,传入其它值则会产生错误。
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
# 6. 总结
到这里,我们的 TypeScript 常用语法学习就告一段落了。当然,TypeScript 还有其他的语法我们并没有讲到。我们只是讲了 TypeScript 的一些常用语法,学会这些知识已经足以开发一般的应用了。如果在使用 TypeScript 开发项目中遇到其他的 TypeScript 语法知识,你可以通过 TypeScript 的 官网文档 (opens new window) 学习。因为学基础最好的方法还是去阅读它的官网文档,并敲上面的小例子。要记住学习一门技术的基础,官网文档永远是最好的第一手资料。
然而,TypeScript 的学习不能仅仅靠看官网文档,你还需要动手实践。在实践中你才能真正掌握 TypeScript。