TypeScript 的 class 类型

116 阅读10分钟

简介 属性的类型 类的属性可以在顶层声明,也可以在构造方法内部声明。

class Point {
  x:number;
  y:number;
}

strictPropertyInitialization,只要打开(默认是打开的),就会检查属性是否设置了初值,如果没有就报错。

如果类的顶层属性不赋值,就会报错。如果不希望出现报错,可以使用非空断言。

class Point {
  x!: number;
  y!: number;
}

readonly 修饰符

如果两个地方都设置了只读属性的值,以构造方法为准

class A {
  readonly id:string = 'foo';

  constructor() {
    this.id = 'bar'; // 正确
  }
}

方法的类型 类的方法就是普通函数,类型声明方式与函数一致。 构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。

class B {
  constructor():object { // 报错
    // ...
  }
}

存取器方法

TypeScript 对存取器有以下规则。

(1)如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。 (2)TypeScript 5.1 版之前,set方法的参数类型,必须兼容get方法的返回值类型,否则报错。

class C {
  _name = '';
  get name():string {
    return this._name;
  }
  set name(value:number|string) {
    this._name = String(value);
  }
}

(3)get方法与set方法的可访问性必须一致,要么都为公开方法,要么都为私有方法。

属性索引

如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。

class MyClass {
  [s:string]: boolean | (() => boolean);
  f() {
    return true;
  }
}

类的 interface 接口

类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。 interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。

interface A {
  get(name:string): boolean;
}

class B implements A {
  get(s) { // s 的类型是 any
    return true;
  }
}

类可以定义接口没有声明的方法和属性。

interface Point {
  x: number;
  y: number;
}

class MyPoint implements Point {
  x = 1;
  y = 1;
  z:number = 1;
}

implements关键字后面,不仅可以是接口,也可以是另一个类。这时,后面的类将被当作接口。

class Car {
  id:number = 1;
  move():void {};
}

class MyCar implements Car {
  id = 2; // 不可省略
  move():void {};   // 不可省略
}

interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。

实现多个接口

类可以实现多个接口(其实是接受多重限制),每个接口之间使用逗号分隔。

class Car implements MotorVehicle, Flyable, Swimmable {
  // ...
}

同时实现多个接口并不是一个好的写法,容易使得代码难以管理,可以使用两种方法替代。

第一种方法是类的继承。

class Car implements MotorVehicle {
}

class SecretCar extends Car implements Flyable, Swimmable {
}

第二种方法是接口的继承。

interface SuperCar extends MotorVehicle, Flyable, Swimmable {
  // ...
}

class SecretCar implements SuperCar {
  // ...
}

发生多重实现时(即一个接口同时实现多个接口),不同接口不能有互相冲突的属性。

类与接口的合并

TypeScript 不允许两个同名的类,但是如果一个类和一个接口同名,那么接口会被合并进类。

class A {
  x:number = 1;
}

interface A {
  y:number;
}

let a = new A();
a.y = 10;

a.x // 1
a.y // 10

Class 类型

实例类型

TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。 对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。

interface MotorVehicle {
}

class Car implements MotorVehicle {
}

// 写法一
const c1:Car = new Car();
// 写法二
const c2:MotorVehicle = new Car();

作为类型使用时,类名只能表示实例的类型,不能表示类的自身类型。

class Point {
  x:number;
  y:number;

  constructor(x:number, y:number) {
    this.x = x;
    this.y = y;
  }
}

// 错误
function createPoint(
  PointClass:Point,
  x: number,
  y: number
) {
  return new PointClass(x, y);
}

TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。

类的自身类型

要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。 类的自身类型可以写成构造函数的形式。 构造函数也可以写成对象形式

function createPoint(PointClass: typeof Point, x: number, y: number): Point {
  return new PointClass(x, y);
}

function createPoint(
  PointClass: new (x: number, y: number) => Point,
  x: number,
  y: number
): Point {
  return new PointClass(x, y);
}

function createPoint(
  PointClass: {
    new (x: number, y: number): Point;
  },
  x: number,
  y: number
): Point {
  return new PointClass(x, y);
}

结构类型原则

Class 也遵循“结构类型原则”。一个对象只要满足 Class 的实例结构,就跟该 Class 属于同一个类型。

class Foo {
  id!:number;
}

function fn(arg:Foo) {
  // ...
}

const bar = {
  id: 10,
  amount: 100,
};

fn(bar); // 正确

如果两个类的实例结构相同,那么这两个类就是兼容的,可以用在对方的使用场合。

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

class Customer {
  name: string;
}

// 正确
const cust:Customer = new Person();

如果某个对象跟某个 class 的实例结构相同,TypeScript 也认为两者的类型相同。

class Person {
  name: string;
}

const obj = { name: 'John' };
const p:Person = obj; // 正确

运算符instanceof不适用于判断某个对象是否跟某个 class 属于同一类型。

空类不包含任何成员,任何其他类都可以看作与空类结构相同。因此,凡是类型为空类的地方,所有类(包括对象)都可以使用。

class Empty {}

function fn(x:Empty) {
  // ...
}

fn({});
fn(window);
fn(fn);

确定两个类的兼容关系时,只检查实例成员,不考虑静态成员和构造方法。

class Point {
  x: number;
  y: number;
  static t: number;
  constructor(x:number) {}
}

class Position {
  x: number;
  y: number;
  z: number;
  constructor(x:string) {}
}

const point:Point = new Position('');

如果类中存在私有成员(private)或保护成员(protected),那么确定兼容关系时,TypeScript 要求私有成员和保护成员来自同一个类,这意味着两个类需要存在继承关系。

// 情况一
class A {
  private name = 'a';
}

class B extends A {
}

const a:A = new B();

// 情况二
class A {
  protected name = 'a';
}

class B extends A {
  protected name = 'b';
}

const a:A = new B();

类的继承

类(这里又称“子类”)可以使用 extends 关键字继承另一个类(这里又称“基类”)的所有属性和方法。 变量a的类型是基类,但是可以赋值为子类的实例

class A {
  greet() {
    console.log("Hello, world!");
  }
}

class B extends A {}

const b = new B();
b.greet(); // "Hello, world!"
const a: A = b;
a.greet();

子类可以覆盖基类的同名方法。

class B extends A {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name}`);
    }
  }
}

子类的同名方法不能与基类的类型定义相冲突。

如果基类包括保护成员(protected修饰符),子类可以将该成员的可访问性设置为公开(public修饰符),也可以保持保护成员不变,但是不能改用私有成员(private修饰符)

class A {
  protected x: string = '';
  protected y: string = '';
  protected z: string = '';
}

class B extends A {
  // 正确
  public x:string = '';

  // 正确
  protected y:string = '';

  // 报错
  private z: string = '';
}

extends关键字后面不一定是类名,可以是一个表达式,只要它的类型是构造函数

// 例一
class MyArray extends Array<number> {}

// 例二
class MyError extends Error {}

// 例三
class A {
  greeting() {
    return 'Hello from A';
  }
}
class B {
  greeting() {
    return 'Hello from B';
  }
}

interface Greeter {
  greeting(): string;
}

interface GreeterConstructor {
  new (): Greeter;
}

function getGreeterBase():GreeterConstructor {
  return Math.random() >= 0.5 ? A : B;
}

class Test extends getGreeterBase() {
  sayHello() {
    console.log(this.greeting());
  }
}

对于那些只设置了类型、没有初值的顶层属性,有一个细节需要注意。 如果编译设置的target设成大于等于ES2022,或者useDefineForClassFields设成true,那么下面代码的执行结果是不一样的。

interface Animal {
  animalStuff: any;
}

interface Dog extends Animal {
  dogStuff: any;
}

class AnimalHouse {
  resident: Animal;

  constructor(animal: Animal) {
    this.resident = animal;
  }
}

class DogHouse extends AnimalHouse {
  resident: Dog;

  constructor(dog: Dog) {
    super(dog);
  }
}
const dog = {
  animalStuff: "animal",
  dogStuff: "dog",
};

const dogHouse = new DogHouse(dog);

console.log(dogHouse.resident); // undefined

可访问性修饰符

public

类的属性和方法默认都是外部可访问的。

private

private修饰符表示私有成员,只能用在当前类的内部,类的实例和子类都不能使用该成员。

子类不能定义父类私有成员的同名成员。

如果在类的内部,当前类的实例可以获取私有成员。private定义的私有成员,并不是真正意义的私有成员。

构造方法也可以是私有的,这就直接防止了使用new命令生成实例对象,只能在类的内部创建实例对象。

class Singleton {
  private static instance?: Singleton;

  private constructor() {}

  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

const s = Singleton.getInstance();

protected

protected修饰符表示该成员是保护成员,只能在类的内部使用该成员,实例无法使用该成员,但是子类内部可以使用。

class A {
  protected x = 1;
}

class B extends A {
  getX() {
    return this.x;
  }
}

const a = new A();
const b = new B();

a.x // 报错
b.getX() // 1

子类不仅可以拿到父类的保护成员,还可以定义同名成员。

class B extends A {
  x = 2;
}

在类的外部,实例对象不能读取保护成员,但是在类的内部可以。

class A {
  protected x = 1;

  f(obj:A) {
    console.log(obj.x);
  }
}

const a = new A();

a.x // 报错
a.f(a) // 1

实例属性的简写形式

构造方法的参数x前面有public修饰符,这时 TypeScript 就会自动声明一个公开属性x,不必在构造方法里面写任何代码,同时还会设置x的值为构造方法的参数值。

class Point {
  x:number;
  y:number;

  constructor(x:number, y:number) {
    this.x = x;
    this.y = y;
  }
}
class Point {
  constructor(
    public x:number,
    public y:number
  ) {}
}

const p = new Point(10, 10);
p.x // 10
p.y // 10

静态成员 # **

类的内部可以使用static关键字,定义静态成员。

静态成员是只能通过类本身使用的成员,不能通过实例对象使用。

静态属性x前面有private修饰符,表示只能在MyClass内部使用

class MyClass {
  private static x = 0;
}

MyClass.x // 报错

publicprotected的静态成员可以被继承。

class A {
  public static x = 1;
  protected static y = 1;
}

class B extends A {
  static getY() {
    return B.y;
  }
}

B.x // 1
B.getY() // 1

泛型类

类也可以写成泛型,使用类型参数。

class Box<Type> {
  contents: Type;

  constructor(value:Type) {
    this.contents = value;
  }
}

const b:Box<string> = new Box('hello!');

静态成员不能使用泛型的类型参数。

class Box<Type> {
  static defaultContents: Type; // 报错
}

抽象类,抽象成员

TypeScript 允许在类的定义前面,加上关键字abstract,表示该类不能被实例化,只能当作其他类的模板。这种类就叫做“抽象类”(abstract class)。

abstract class A {
  id = 1;
}

class B extends A {
  amount = 100;
}

const b = new B();

b.id // 1
b.amount // 100

抽象类可以继承其他抽象类。 抽象类的内部可以有已经实现好的属性和方法,也可以有还未实现的属性和方法。后者就叫做“抽象成员”(abstract member),即属性名和方法名有abstract关键字,表示该方法需要子类实现。如果子类没有实现抽象成员,就会报错。

abstract class A {
  abstract foo:string;
  bar:string = '';
}

class B extends A {
  foo = 'b';
}

抽象方法

abstract class A {
  abstract execute():string;
}

class B extends A {
  execute() {
    return `B executed`;
  }
}

(1)抽象成员只能存在于抽象类,不能存在于普通类。

(2)抽象成员不能有具体实现的代码。也就是说,已经实现好的成员前面不能加abstract关键字。

(3)抽象成员前也不能有private修饰符,否则无法在子类中实现该成员。

(4)一个子类最多只能继承一个抽象类。

this 问题

TypeScript 允许函数增加一个名为this的参数

class A {
  name = 'A';

  getName(this: A) {
    return this.name;
  }
}

const a = new A();
const b = a.getName;

b() // 报错

TypeScript 提供了一个noImplicitThis编译选项。如果打开了这个设置项,如果this的值推断为any类型,就会报错。

在类的内部,this本身也可以当作类型使用,表示当前类的实例对象。

class Box {
  contents:string = '';

  set(value:string):this {
    this.contents = value;
    return this;
  }
}

this类型不允许应用于静态成员。

有些方法返回一个布尔值,表示当前的this是否属于某种类型。这时,这些方法的返回值类型可以写成this is Type的形式,其中用到了is运算符。

class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }

  isDirectory(): this is Directory {
    return this instanceof Directory;
  }

  // ...
}