返回

揭秘 TypeScript 高级类型的高端应用之道

前端

TypeScript 中的类型兼容性

在 TypeScript 的世界里,类型兼容性就像是一套规则,它决定了一个类型能否被赋予另一个类型的值。打个比方,就像不同型号的插座和插头,只有匹配的才能顺利通电。TypeScript 中的类型兼容性主要分为两种:结构化类型兼容性和标明类型兼容性。

结构化类型系统

结构化类型兼容性,顾名思义,它关注的是类型的结构,就像我们判断两个盒子是否一样,主要看它们的大小和形状,而不太在意它们的颜色或者材质。如果两个类型拥有相同的结构,那么它们就被认为是兼容的。举个例子:

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

const person1: Person = {
  name: 'Alice',
  age: 20,
};

const person2: Person = {
  name: 'Bob',
  age: 30,
};

person1 = person2; // 赋值成功,因为 person1 和 person2 拥有相同的结构

在这个例子中,Person 接口定义了两个属性:nameageperson1person2 都是 Person 类型的对象,它们都拥有 nameage 这两个属性,因此它们是兼容的,可以互相赋值。

标明类型系统

与结构化类型系统不同,标明类型系统更看重类型的“出身”。就像我们判断两个人是否亲戚,要看他们的家谱一样,标明类型系统会检查两个类型是否拥有相同的名称或者来源。即使两个类型拥有相同的结构,如果它们的名称不同,它们也不被认为是兼容的。举个例子:

class Person {
  name: string;
  age: number;
}

const person1 = new Person();
person1.name = 'Alice';
person1.age = 20;

const person2 = new Person();
person2.name = 'Bob';
person2.age = 30;

person1 = person2; // 编译错误,因为 person1 和 person2 是不同的对象实例

在这个例子中,person1person2 都是 Person 类的实例,它们都拥有 nameage 这两个属性,但是它们是不同的对象实例,因此它们不兼容,不能互相赋值。

TypeScript 高级类型技巧

TypeScript 的类型系统就像一个工具箱,里面装满了各种各样的高级类型技巧,能够帮助我们编写更加健壮和可扩展的代码。下面,我们就来介绍一些常用的高级类型技巧。

类型保护

在 TypeScript 中,我们经常会遇到需要判断一个变量具体类型的情况。例如,一个变量可能是字符串类型,也可能是数字类型,我们需要根据它的具体类型来进行不同的处理。这时候,就需要用到类型保护。

TypeScript 提供了多种类型保护的方式,例如:

  • 类型断言 : 我们可以使用 as 来告诉 TypeScript 编译器,一个变量的类型是什么。例如:
let value: any = "hello";

let strLength: number = (value as string).length; 
  • 类型守卫 : 我们可以使用 typeofinstanceof 等操作符来判断一个变量的类型。例如:
function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); 
  } else {
    console.log(value.toFixed(2)); 
  }
}
  • in 操作符 : 我们可以使用 in 操作符来判断一个对象是否包含某个属性。例如:
interface Person {
  name: string;
  age: number;
}

interface Animal {
  name: string;
  breed: string;
}

function printName(obj: Person | Animal) {
  if ("age" in obj) {
    console.log(obj.name); 
  } else {
    console.log(obj.name); 
  }
}

类型别名

有时候,一个类型可能会很长或者很复杂,为了方便使用,我们可以给它起一个别名。就像我们给朋友起外号一样,类型别名可以让代码更简洁易懂。

我们可以使用 type 关键字来定义类型别名。例如:

type StringOrNumber = string | number;

let value: StringOrNumber = "hello";
value = 123;

在这个例子中,我们定义了一个类型别名 StringOrNumber,它表示字符串类型或数字类型。然后,我们可以使用 StringOrNumber 来声明变量 value

接口

接口就像一份协议,它规定了一个对象应该具备哪些属性和方法。通过接口,我们可以规范对象的结构,提高代码的可维护性和可扩展性。

我们可以使用 interface 关键字来定义接口。例如:

interface Person {
  name: string;
  age: number;
  greet(): void;
}

let person: Person = {
  name: "Alice",
  age: 20,
  greet: function() {
    console.log("Hello, my name is " + this.name);
  }
};

在这个例子中,我们定义了一个 Person 接口,它规定了一个对象应该具备 nameagegreet 这三个成员。然后,我们创建了一个 person 对象,它符合 Person 接口的规范。

类是面向对象编程中的一个重要概念,它可以用来创建对象的模板。通过类,我们可以将对象的属性和方法封装在一起,提高代码的复用性和可维护性。

我们可以使用 class 关键字来定义类。例如:

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log("Hello, my name is " + this.name);
  }
}

let person = new Person("Alice", 20);
person.greet();

在这个例子中,我们定义了一个 Person 类,它包含 nameage 两个属性,以及一个 greet 方法。然后,我们使用 new 关键字创建了一个 Person 类的实例 person,并调用了它的 greet 方法。

联合类型和交叉类型

联合类型表示一个变量可以是多种类型之一,而交叉类型表示一个变量必须同时满足多种类型的要求。

我们可以使用 | 操作符来定义联合类型,使用 & 操作符来定义交叉类型。例如:

// 联合类型
type StringOrNumber = string | number;

let value: StringOrNumber = "hello";
value = 123;

// 交叉类型
interface Person {
  name: string;
  age: number;
}

interface Worker {
  companyId: string;
}

type Employee = Person & Worker;

let employee: Employee = {
  name: "Alice",
  age: 20,
  companyId: "ABC"
};

元组类型

元组类型表示一个固定长度的数组,数组中的每个元素都可以是不同的类型。

我们可以使用 [] 操作符来定义元组类型。例如:

type PersonTuple = [string, number];

let person: PersonTuple = ["Alice", 20];

在这个例子中,我们定义了一个元组类型 PersonTuple,它表示一个包含两个元素的数组,第一个元素是字符串类型,第二个元素是数字类型。

枚举类型

枚举类型表示一组命名的常量。

我们可以使用 enum 关键字来定义枚举类型。例如:

enum Color {
  Red,
  Green,
  Blue
}

let color: Color = Color.Red;

在这个例子中,我们定义了一个枚举类型 Color,它包含三个常量:RedGreenBlue

可选类型

可选类型表示一个属性可以存在,也可以不存在。

我们可以使用 ? 操作符来定义可选类型。例如:

interface Person {
  name: string;
  age?: number;
}

let person: Person = {
  name: "Alice"
};

在这个例子中,我们定义了一个 Person 接口,它包含一个 name 属性和一个可选的 age 属性。

只读类型

只读类型表示一个属性一旦被赋值后,就不能再被修改。

我们可以使用 readonly 关键字来定义只读类型。例如:

interface Person {
  readonly name: string;
  age: number;
}

let person: Person = {
  name: "Alice",
  age: 20
};

person.age = 30; // 可以修改 age 属性
person.name = "Bob"; // 编译错误,不能修改 name 属性

泛型

泛型是一种可以被参数化的类型,它可以让我们编写更加通用的代码。

我们可以使用 <T> 语法来定义泛型。例如:

function identity<T>(arg: T): T {
  return arg;
}

let str: string = identity<string>("hello");
let num: number = identity<number>(123);

在这个例子中,我们定义了一个泛型函数 identity,它可以接受任何类型的参数,并返回相同类型的结果。

常见问题解答

1. 什么是类型兼容性?

类型兼容性是指 TypeScript 中的一种规则,它决定了某个类型的值是否可以赋给另一个类型的变量。

2. TypeScript 中有哪些类型保护机制?

TypeScript 中的类型保护机制包括类型断言、类型守卫、in 操作符和 typeof 操作符等。

3. 如何定义类型别名?

可以使用 type 关键字来定义类型别名,例如 type StringOrNumber = string | number;

4. 接口和类的区别是什么?

接口是一种对象结构的类型,而类是一种创建对象的模板。接口只能对象的属性和方法,而类还可以包含构造函数、静态方法等成员。

5. 什么是泛型?

泛型是一种可以被参数化的类型,它可以让我们编写更加通用的代码。