五、联合类型和类型别名

5.1 联合类型

联合类型通常与 nullundefined 一起使用:

  1. const sayHello = (name: string | undefined) => {
  2. /* ... */
  3. };

例如,这里 name 的类型是 string | undefined 意味着可以将 stringundefined 的值传递给sayHello 函数。

  1. sayHello("Semlinker");
  2. sayHello(undefined);

通过这个示例,你可以凭直觉知道类型 A 和类型 B 联合后的类型是同时接受 A 和 B 值的类型。

5.2 可辨识联合

TypeScript 可辨识联合(Discriminated Unions)类型,也称为代数数据类型或标签联合类型。它包含 3 个要点:可辨识、联合类型和类型守卫。
这种类型的本质是结合联合类型和字面量类型的一种类型保护方法。如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。
1.可辨识
可辨识要求联合类型中的每个元素都含有一个单例类型属性,比如:

  1. enum CarTransmission {
  2. Automatic = 200,
  3. Manual = 300
  4. }
  5. interface Motorcycle {
  6. vType: "motorcycle"; // discriminant
  7. make: number; // year
  8. }
  9. interface Car {
  10. vType: "car"; // discriminant
  11. transmission: CarTransmission
  12. }
  13. interface Truck {
  14. vType: "truck"; // discriminant
  15. capacity: number; // in tons
  16. }

在上述代码中,我们分别定义了 MotorcycleCarTruck 三个接口,在这些接口中都包含一个 vType 属性,该属性被称为可辨识的属性,而其它的属性只跟特性的接口相关。
2.联合类型
基于前面定义了三个接口,我们可以创建一个 Vehicle 联合类型:

  1. type Vehicle = Motorcycle | Car | Truck;

现在我们就可以开始使用 Vehicle 联合类型,对于 Vehicle 类型的变量,它可以表示不同类型的车辆。
3.类型守卫
下面我们来定义一个 evaluatePrice 方法,该方法用于根据车辆的类型、容量和评估因子来计算价格,具体实现如下:

  1. const EVALUATION_FACTOR = Math.PI;
  2. function evaluatePrice(vehicle: Vehicle) {
  3. return vehicle.capacity * EVALUATION_FACTOR;
  4. }
  5. const myTruck: Truck = { vType: "truck", capacity: 9.5 };
  6. evaluatePrice(myTruck);

对于以上代码,TypeScript 编译器将会提示以下错误信息:

  1. Property 'capacity' does not exist on type 'Vehicle'.
  2. Property 'capacity' does not exist on type 'Motorcycle'.

原因是在 Motorcycle 接口中,并不存在 capacity 属性,而对于 Car 接口来说,它也不存在 capacity 属性。那么,现在我们应该如何解决以上问题呢?这时,我们可以使用类型守卫。下面我们来重构一下前面定义的 evaluatePrice 方法,重构后的代码如下:

  1. function evaluatePrice(vehicle: Vehicle) {
  2. switch(vehicle.vType) {
  3. case "car":
  4. return vehicle.transmission * EVALUATION_FACTOR;
  5. case "truck":
  6. return vehicle.capacity * EVALUATION_FACTOR;
  7. case "motorcycle":
  8. return vehicle.make * EVALUATION_FACTOR;
  9. }
  10. }

在以上代码中,我们使用 switchcase 运算符来实现类型守卫,从而确保在 evaluatePrice 方法中,我们可以安全地访问 vehicle 对象中的所包含的属性,来正确的计算该车辆类型所对应的价格。

5.3 类型别名

类型别名用来给一个类型起个新名字。

  1. type Message = string | string[];
  2. let greet = (message: Message) => {
  3. // ...
  4. };

六、交叉类型

TypeScript 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

  1. interface IPerson {
  2. id: string;
  3. age: number;
  4. }
  5. interface IWorker {
  6. companyId: string;
  7. }
  8. type IStaff = IPerson & IWorker;
  9. const staff: IStaff = {
  10. id: 'E1006',
  11. age: 33,
  12. companyId: 'EFT'
  13. };
  14. console.dir(staff)

在上面示例中,我们首先为 IPerson 和 IWorker 类型定义了不同的成员,然后通过 & 运算符定义了 IStaff 交叉类型,所以该类型同时拥有 IPerson 和 IWorker 这两种类型的成员。

七、TypeScript 函数

7.1 TypeScript 函数与 JavaScript 函数的区别

TypeScript JavaScript
含有类型 无类型
箭头函数 箭头函数(ES2015)
函数类型 无函数类型
必填和可选参数 所有参数都是可选的
默认参数 默认参数
剩余参数 剩余参数
函数重载 无函数重载

7.2 箭头函数

1.常见语法

  1. myBooks.forEach(() => console.log('reading'));
  2. myBooks.forEach(title => console.log(title));
  3. myBooks.forEach((title, idx, arr) =>
  4. console.log(idx + '-' + title);
  5. );
  6. myBooks.forEach((title, idx, arr) => {
  7. console.log(idx + '-' + title);
  8. });

2.使用示例

  1. // 未使用箭头函数
  2. function Book() {
  3. let self = this;
  4. self.publishDate = 2016;
  5. setInterval(function () {
  6. console.log(self.publishDate);
  7. }, 1000);
  8. }
  9. // 使用箭头函数
  10. function Book() {
  11. this.publishDate = 2016;
  12. setInterval(() => {
  13. console.log(this.publishDate);
  14. }, 1000);
  15. }

7.3 参数类型和返回类型

  1. function createUserId(name: string, id: number): string {
  2. return name + id;
  3. }

7.4 函数类型

  1. let IdGenerator: (chars: string, nums: number) => string;
  2. function createUserId(name: string, id: number): string {
  3. return name + id;
  4. }
  5. IdGenerator = createUserId;

7.5 可选参数及默认参数

  1. // 可选参数
  2. function createUserId(name: string, id: number, age?: number): string {
  3. return name + id;
  4. }
  5. // 默认参数
  6. function createUserId(
  7. name: string = "Semlinker",
  8. id: number,
  9. age?: number
  10. ): string {
  11. return name + id;
  12. }

在声明函数时,可以通过 ? 号来定义可选参数,比如 age?: number 这种形式。在实际使用时,需要注意的是可选参数要放在普通参数的后面,不然会导致编译错误。

7.6 剩余参数

  1. function push(array, ...items) {
  2. items.forEach(function (item) {
  3. array.push(item);
  4. });
  5. }
  6. let a = [];
  7. push(a, 1, 2, 3);

7.7 函数重载

函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。要解决前面遇到的问题,方法就是为同一个函数提供多个函数类型定义来进行函数重载,编译器会根据这个列表去处理函数的调用。

  1. function add(a: number, b: number): number;
  2. function add(a: string, b: string): string;
  3. function add(a: string, b: number): string;
  4. function add(a: number, b: string): string;
  5. function add(a: Combinable, b: Combinable) {
  6. if (typeof a === "string" || typeof b === "string") {
  7. return a.toString() + b.toString();
  8. }
  9. return a + b;
  10. }

在以上代码中,我们为 add 函数提供了多个函数类型定义,从而实现函数的重载。之后,可恶的错误消息又消失了,因为这时 result 变量的类型是 string 类型。在 TypeScript 中除了可以重载普通函数之外,我们还可以重载类中的成员方法。
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。下面我们来举一个成员方法重载的例子:

  1. class Calculator {
  2. add(a: number, b: number): number;
  3. add(a: string, b: string): string;
  4. add(a: string, b: number): string;
  5. add(a: number, b: string): string;
  6. add(a: Combinable, b: Combinable) {
  7. if (typeof a === "string" || typeof b === "string") {
  8. return a.toString() + b.toString();
  9. }
  10. return a + b;
  11. }
  12. }
  13. const calculator = new Calculator();
  14. const result = calculator.add("Semlinker", " Kakuqo");

这里需要注意的是,当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ } 并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。