传统的面向对象语言都是基于类的,而JavaScript是基于原型的。在ES6中拥有了class关键字,虽然它的本质依旧是构造函数,但是能够让开发者更舒服的使用class了。 TypeScript 作为 JavaScript 的超集,自然也是支持 class 全部特性的,并且还可以对类的属性、方法等进行静态类型检测。下面就来看看 TypeScript 中的类类型。

一、类的概念

1. 类的使用

在开发过程中,任何实体都可以被抽象为一个使用类表达的类似对象的数据结构,这个数据结构既包含属性,又包含方法。在TypeScript 中可以这样来抽象一个坐标点类:

  1. class Point {
  2. x: number;
  3. y: number;
  4. constructor(x: number, y: number) {
  5. this.x = x;
  6. this.y = y;
  7. }
  8. getPosition() {
  9. return `(${this.x}, ${this.y})`;
  10. }
  11. }
  12. const point = new Point(1, 2);
  13. point.getPosition() // (1, 2)

这里定义了一个 Point 坐标点类,它拥有两个number类型的属性 x 和 y,一个构造器函数和一个getPosition方法。后面通过new实例化了一个Point,并将实例赋值给变量point,最后通过实例调用了类中定义的 getPosition 方法。

在ES6之前,需要使用函数+原型链的形式进行模拟定义类:

  1. function Point(x, y) {
  2. this.x = x;
  3. this.y = y;
  4. }
  5. Point.prototype.getPosition = function() {
  6. return `(${this.x}, ${this.y})`;
  7. }
  8. const point = new Point(1, 2);
  9. point.getPosition() // (1, 2)

开始定义了 Point 类的构造函数,并在构造函数内部定义了 x 和 y 属性,后面通过 Point 的原型链添加了 getPosition 方法。这样也模拟实现了 class 的功能,但是看起来麻烦很多,并且缺少静态类型检测。因此,类是 TypeScript 编程中十分有用且不得不掌握的工具。

2. 类的继承

下面来看一下作为面向对象的三大也行之一的继承,在 TypeScript 中,可以使用 extends 关键字来定义类继承的抽象模式:

  1. class A {
  2. name: string;
  3. age: number;
  4. constructor(name: string, age: number) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. getName() {
  9. return this.name;
  10. }
  11. }
  12. class B extends A {
  13. job: string;
  14. constructor(name: string, age: number) {
  15. super(name, age);
  16. this.job = "IT";
  17. }
  18. getJob() {
  19. return this.job;
  20. }
  21. getNameAndJob() {
  22. return super.getName() + this.job;
  23. }
  24. }
  25. var b = new B("Tom", 20);
  26. console.log(b.name);
  27. console.log(b.age);
  28. console.log(b.getName());
  29. console.log(b.getJob());
  30. console.log(b.getNameAndJob());
  31. //输出:Tom,20,Tom,IT,TomIT

如上,B继承A,那B被称为父类(超类),A被称为子类(派生类)。这就是类最基本的继承用法,B就是一个派生类,它派生自A类,此时B的实例继承了基类A的属性和方法。因此,实例 b 支持 name、age、getName 等属性和方法。

需要注意,派生类如果包含一个构造函数constructor,则必须在构造函数中调用 super() 方法,这是 TypeScript 强制执行的一条重要规则。否则就会报错:Constructors for derived classes must contain a ‘super’ call.

那这个 super() 有作用呢?其实这里的 super 函数会调用基类的构造函数,当我们把鼠标放在super方法上面时,可以看到一个提示,它的类型是基类 A 的构造函数:constructorA(name: string,age: number): A。并且指明了需要传递两个参数,不然TypeScript就会报错。

二、类的修饰符

1. 访问修饰符

在 ES6 标准类的定义中,默认情况下,定义在实例的属性和方法会在创建实例后添加到实例上;而如果是定义在类里没有定义在 this 上的方法,实例可以继承这个方法;而如果使用 static 修饰符定义的属性和方法,是静态属性和静态方法,实例是没法访问和继承到的。

传统面向对象语言通常都有访问修饰符,可以通过修饰符来控制可访问性。TypeScript 中有三类访问修饰符:

  • public:修饰的是在任何地方可见、公有的属性或方法;
  • private:修饰的是仅在同一类中可见、私有的属性或方法;
  • protected:修饰的是仅在类自身及子类中可见、受保护的属性或方法。

    (1)public

    public表示公共的,用来指定在创建实例后可以通过实例访问的,也就是类定义的外部可以访问的属性和方法。默认是 public,但是 TSLint 可能会要求必须用限定符来表明这个属性或方法是什么类型的: ``typescript class Point { public x: number; public y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } public getPosition() { return(${this.x}, ${this.y})`; } }

const point = new Point(1, 2) console.log(point.x) // 1 console.log(point.y) // 2 console.log(point.getPosition()) // (1, 2)

  1. <a name="VBUgF"></a>
  2. #### (2)private
  3. `private`修饰符表示私有的,它修饰的属性在类的定义外面是没法访问的:
  4. ```typescript
  5. class Parent {
  6. private age: number;
  7. constructor(age: number) {
  8. this.age = age;
  9. }
  10. }
  11. const p = new Parent(18);
  12. console.log(p); // { age: 18 }
  13. console.log(p.age); // error Property 'age' is private and only accessible within class 'Parent'.
  14. console.log(Parent.age); // error Property 'age' does not exist on type 'typeof Parent'.
  15. class Child extends Parent {
  16. constructor(age: number) {
  17. super(age);
  18. console.log(super.age); // Only public and protected methods of the base class are accessible via the 'super' keyword.
  19. }
  20. }

这里 age 属性使用 private 修饰符修饰,说明它是私有属性,打印创建的实例对象 p,发现它是有属性 age 的,但是当试图访问 p 的 age 属性时,编译器会报错,私有属性只能在类 Parent 中访问。

对于 super.age 的报错,在不同类型的方法里 super 作为对象代表着不同的含义,这里在 constructor 中访问 super,这的 super 相当于父类本身,使用 private 修饰的属性,在子类中是无法访问的。

(3) protected

protected是受保护修饰符,和private有些相似,但有一点不同,protected修饰的成员在继承该类的子类中可以访问。把上面那个例子父类 Parent 的 age 属性的修饰符 private 替换为

  1. class Parent {
  2. protected age: number;
  3. constructor(age: number) {
  4. this.age = age;
  5. }
  6. protected getAge() {
  7. return this.age;
  8. }
  9. }
  10. const p = new Parent(18);
  11. console.log(p.age); // error Property 'age' is protected and only accessible within class 'Parent' and its subclasses.
  12. console.log(Parent.age); // error Property 'age' does not exist on type 'typeof Parent'.
  13. class Child extends Parent {
  14. constructor(age: number) {
  15. super(age);
  16. console.log(super.age); // error Only public and protected methods of the base class are accessible via the 'super' keyword.
  17. console.log(super.getAge());
  18. }
  19. }
  20. new Child(18)

protected还能用来修饰 constructor 构造函数,加了protected修饰符之后,这个类就不能再用来创建实例,只能被子类继承,ES6 的类需要用new.target来自行判断,而 TS 则只需用 protected 修饰符即可:

  1. class Parent {
  2. protected constructor() {
  3. //
  4. }
  5. }
  6. const p = new Parent(); // error Constructor of class 'Parent' is protected and only accessible within the class declaration.
  7. class Child extends Parent {
  8. constructor() {
  9. super();
  10. }
  11. }
  12. const c = new Child();

2. 只读修饰符

在类中可以使用readonly关键字将属性设置为只读:

  1. class UserInfo {
  2. readonly name: string;
  3. constructor(name: string) {
  4. this.name = name;
  5. }
  6. }
  7. const user = new UserInfo("TypeScript");
  8. user.name = "haha"; // error Cannot assign to 'name' because it is a read-only property

设置为只读的属性,实例只能读取这个属性值,但不能修改。

需要注意,如果只读修饰符和可见性修饰符同时出现,需要将只读修饰符写在可见修饰符后面。

三、类的类型

1. 属性类型

(1)参数属性

在上面的例子中,都是在类的定义的顶部初始化实例属性,在 constructor 里接收参数然后对实例属性进行赋值,可以使用参数属性来简化这个过程。参数属性就是在 constructor 构造函数的参数前面加上访问限定符:

  1. class A {
  2. constructor(name: string) {}
  3. }
  4. const a = new A("aaa");
  5. console.log(a.name); // error 类型“A”上不存在属性“name”
  6. class B {
  7. constructor(public name: string) {}
  8. }
  9. const b = new B("bbb");
  10. console.log(b.name); // "bbb"

可以看到,在定义类 B 时,构造函数有一个参数 name,这个 name 使用访问修饰符 public 修饰,此时即为 name 声明了参数属性,也就无需再显式地在类中初始化这个属性了。

(2)静态属性

在 TypeScript 中和 ES6 中一样使用static关键字来指定属性或方法是静态的,实例将不会添加这个静态属性,也不会继承这个静态方法。可以使用修饰符和 static 关键字来指定一个属性或方法:

  1. class Parent {
  2. public static age: number = 18;
  3. public static getAge() {
  4. return Parent.age;
  5. }
  6. constructor() {
  7. //
  8. }
  9. }
  10. const p = new Parent();
  11. console.log(p.age); // error Property 'age' is a static member of type 'Parent'
  12. console.log(Parent.age); // 18

如果使用了 private 修饰道理和之前的一样:

  1. class Parent {
  2. public static getAge() {
  3. return Parent.age;
  4. }
  5. private static age: number = 18;
  6. constructor() {
  7. //
  8. }
  9. }
  10. const p = new Parent();
  11. console.log(p.age); // error Property 'age' is a static member of type 'Parent'
  12. console.log(Parent.age); // error 属性“age”为私有属性,只能在类“Parent”中访问。

(3)可选类属性

TypeScript 还支持可选类属性,也是使用?符号来标记:

  1. class Info {
  2. name: string;
  3. age?: number;
  4. constructor(name: string, age?: number, public sex?: string) {
  5. this.name = name;
  6. this.age = age;
  7. }
  8. }
  9. const info1 = new Info("TypeScript");
  10. const info2 = new Info("TypeScript", 18);
  11. const info3 = new Info("TypeScript", 18, "man");

2. 类的类型

类的类型和函数类似,即在声明类时,同时声明了一个特殊的类型,这个类型的名字就是类名,表示类实例的类型;在定义类的时候,声明的除构造函数外所有属性、方法的类型就是这个特殊类型的成员。

定义一个类,并创建实例后,这个实例的类型就是创建他的类:

  1. class People {
  2. constructor(public name: string) {}
  3. }
  4. let people: People = new People("TypeScript");

创建实例时指定 p 的类型为 People 并不是必须的,TS 会推断出他的类型。虽然指定了类型,但是当再定义一个和 People 类同样实现的类 Animal,并且创建实例赋值给 p 的时候,是没有问题的:

  1. class Animal {
  2. constructor(public name: string) {}
  3. }
  4. let people = new Animal("JavaScript");

所以,如果想实现对创建实例的类的判断,还是需要用到instanceof关键字。

四、类的使用

1. 抽象类

抽象类一般用来被其他类继承,而不直接用它创建实例。抽象类和类内部定义抽象方法,使用abstract关键字:

  1. abstract class People {
  2. constructor(public name: string) {}
  3. abstract printName(): void;
  4. }
  5. class Man extends People {
  6. constructor(name: string) {
  7. super(name);
  8. this.name = name;
  9. }
  10. printName() {
  11. console.log(this.name);
  12. }
  13. }
  14. const m = new Man(); // error Expected 1 arguments, but got 0.
  15. const man = new Man("TypeScript");
  16. man.printName(); // 'TypeScript'
  17. const p = new People("TypeScript"); // error Cannot create an instance of an abstract class.

这里定义了一个抽象类 People,在抽象类里定义 constructor 方法必须传入一个字符串类型参数,并把这个 name 参数值绑定在创建的实例上;使用abstract关键字定义一个抽象方法 printName,这个定义可以指定参数,指定参数类型,指定返回类型。当直接使用抽象类 People 实例化的时候,就会报错,只能创建一个继承抽象类的子类,使用子类来实例化。

再看下面的例子:

  1. abstract class People {
  2. constructor(public name: string) {}
  3. abstract printName(): void;
  4. }
  5. class Man extends People { // error Non-abstract class 'Man' does not implement inherited abstract member 'printName' from class 'People'
  6. constructor(name: string) {
  7. super(name);
  8. this.name = name;
  9. }
  10. }
  11. const m = new Man("TypeScript");
  12. m.printName(); // error m.printName is not a function

可以看到,第5行报错:非抽象类“Man”不会实现继承自“People”类的抽象成员”printName”。在抽象类里定义的抽象方法,在子类中是不会继承的,所以在子类中必须实现该方法的定义。

TypeScript 的abstract关键字不仅可以标记类和类里面的方法,还可以标记类中定义的属性和存取器:

  1. abstract class People {
  2. abstract name: string;
  3. abstract get insideName(): string;
  4. abstract set insideName(value: string);
  5. }
  6. class Pp extends People {
  7. name: string;
  8. insideName: string;
  9. }

注意:抽象方法和抽象存取器都不能包含实际的代码块。

2. 存取器

存取器就是 ES6 标准中的存值函数和取值函数,也就是在设置属性值的时候调用的函数,和在访问属性值的时候调用的函数,用法和写法和 ES6 的没有区别,可以通过getter、setter截取对类成员的读写访问:

  1. class UserInfo {
  2. private name: string;
  3. constructor() {}
  4. get userName() {
  5. return this.name;
  6. }
  7. set userName(value) {
  8. console.log(`setter: ${value}`);
  9. this.name = value;
  10. }
  11. }
  12. const user = new UserInfo();
  13. user.name = "TypeScript"; // "setter: TypeScript"
  14. console.log(user.name); // "TypeScript"

五、类的接口

1. 类类型接口

使用接口可以强制一个类的定义必须包含某些内容:

  1. interface FoodInterface {
  2. type: string;
  3. }
  4. class FoodClass implements FoodInterface {
  5. // error Property 'type' is missing in type 'FoodClass' but required in type 'FoodInterface'
  6. static type: string;
  7. constructor() {}
  8. }

上面接口 FoodInterface 要求使用该接口的值必须有一个 type 属性,定义的类 FoodClass 要使用接口,需要使用关键字implementsimplements关键字用来指定一个类要继承的接口,如果是接口和接口、类和类直接的继承,使用extends,如果是类继承接口,则用implements。

注意,接口检测的是使用该接口定义的类创建的实例,所以上面例子中虽然定义了静态属性 type,但静态属性不会添加到实例上,所以还是报错,可以这样改:

  1. interface FoodInterface {
  2. type: string;
  3. }
  4. class FoodClass implements FoodInterface {
  5. constructor(public type: string) {}
  6. }

当然也可以使用抽象类实现:

  1. abstract class FoodAbstractClass {
  2. abstract type: string;
  3. }
  4. class Food extends FoodAbstractClass {
  5. constructor(public type: string) {
  6. super();
  7. }
  8. }

2. 接口继承类

接口可以继承一个类,当接口继承了该类后,会继承类的成员,但是不包括其实现,也就是只继承成员以及成员类型。接口还会继承类的privateprotected修饰的成员,当接口继承的这个类中包含这两个修饰符修饰的成员时,这个接口只可被这个类或他的子类实现:

  1. class A {
  2. protected name: string;
  3. }
  4. interface I extends A {}
  5. class B implements I {} // error Property 'name' is missing in type 'B' but required in type 'I'
  6. class C implements I {
  7. // error 属性“name”受保护,但类型“C”并不是从“A”派生的类
  8. name: string;
  9. }
  10. class D extends A implements I {
  11. getName() {
  12. return this.name;
  13. }
  14. }

六、其他

1. 在泛型中使用类类型

先来看个例子:

  1. const create = <T>(c: { new (): T }): T => {
  2. return new c();
  3. };
  4. class Info {
  5. age: number;
  6. }
  7. create(Info).age;
  8. create(Info).name; // error 类型“Info”上不存在属性“name”

这里创建了一个 create 函数,传入的参数是一个类,返回的是一个类创建的实例,注意:

  • 参数 c 的类型定义中,new()代表调用类的构造函数,他的类型也就是类创建实例后的实例的类型。
  • return new c()这里使用传进来的类 c 创建一个实例并返回,返回的实例类型也就是函数的返回值类型。

所以通过这个定义,TypeScript 就知道,调用 create 函数,传入的和返回的值都应该是同一个类类型。