美文网首页
TypeScript 详解之 TypeScript 的 clas

TypeScript 详解之 TypeScript 的 clas

作者: you的日常 | 来源:发表于2023-08-11 08:55 被阅读0次

简介

类(class)是面向对象编程的基本构件,封装了属性和方法,TypeScript 给予了全面支持。

属性的类型

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

对于顶层声明的属性,可以在声明时同时给出类型。

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

上面声明中,属性xy的类型都是number

如果不给出类型,TypeScript 会认为xy的类型都是any

class Point {
  x;
  y;
}

上面示例中,xy的类型都是any

如果声明时给出初值,可以不写类型,TypeScript 会自行推断属性的类型。

class Point {
  x = 0;
  y = 0;
}

上面示例中,属性xy的类型都会被推断为 number。

TypeScript 有一个配置项strictPropertyInitialization,只要打开,就会检查属性是否设置了初值,如果没有就报错。

如果你打开了这个设置,但是某些情况下,不是在声明时赋值或在构造方法里面赋值,为了防止这个设置报错,可以使用非空断言。

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

上面示例中,属性xy没有初值,但是属性名后面添加了感叹号,表示这两个属性肯定不会为空,所以 TypeScript 就不报错了,详见《类型断言》一章。

readonly 修饰符

属性名前面加上 readonly 修饰符,就表示该属性是只读的。实例对象不能修改这个属性。

class A {
  readonly id = 'foo';
}

const a = new A();
a.id = 'bar'; // 报错

上面示例中,id属性前面有 readonly 修饰符,实例对象修改这个属性就会报错。

readonly 属性的初始值,可以写在顶层属性,也可以写在构造方法里面。

class A {
  readonly id:string;

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

上面示例中,构造方法内部设置只读属性的初值,这是可以的。

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

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

上面示例中,构造方法修改只读属性的值也是可以的。或者说,如果两个地方都设置了只读属性的值,以构造方法为准。在其他方法修改只读属性都会报错。

方法的类型

类的方法就是普通函数,类型声明方式与函数一致。

class Point {
  x:number;
  y:number;

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

  add(point:Point) {
    return new Point(
      this.x + point.x,
      this.y + point.y
    );
  }
}

上面示例中,构造方法constructor()和普通方法add()都注明了参数类型,但是省略了返回值类型,因为 TypeScript 可以自己推断出来。

类的方法跟普通函数一样,可以使用参数默认值,以及函数重载。

下面是参数默认值的例子。

class Point {
  x: number;
  y: number;

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

上面示例中,如果新建实例时,不提供属性xy的值,它们都等于默认值0

下面是函数重载的例子。

class Point {
  constructor(x:number, y:string);
  constructor(s:string);
  constructor(xs:number|string, y?:string) {
    // ...
  }
}

上面示例中,构造方法可以接受一个参数,也可以接受两个参数,采用函数重载进行类型声明。

另外,构造方法不能声明返回值类型,否则报错,因为它总是返回实例对象。

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

上面示例中,构造方法声明了返回值类型object,导致报错。

存取器方法

存取器(accessor)是特殊的类方法,包括取值器(getter)和存值器(setter)两种方法。

它们用于读写某个属性,取值器用来读取属性,存值器用来写入属性。

class C {
  _name = '';
  get name() {
    return this._name;
  }
  set name(value) {
    this._name = value;
  }
}

上面示例中,get name()是取值器,其中get是关键词,name是属性名。外部读取name属性时,实例对象会自动调用这个方法,该方法的返回值就是name属性的值。

set name()是存值器,其中set是关键词,name是属性名。外部写入name属性时,实例对象会自动调用这个方法,并将所赋的值作为函数参数传入。

TypeScript 对存取器有以下规则。

(1)如果某个属性只有get方法,没有set方法,那么该属性自动成为只读属性。

class C {
  _name = 'foo';

  get name() {
    return this._name;
  }
}

const c = new C();
c.name = 'bar'; // 报错

上面示例中,name属性没有set方法,对该属性赋值就会报错。

(2)set方法的参数类型,必须兼容get方法的返回值类型,否则报错。

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

上面示例中,get方法的返回值类型是字符串,与set方法参数类型不兼容,导致报错。

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

上面示例中,set方法的参数类型(number|return)兼容get方法的返回值类型(string),这是允许的。但是,最终赋值的时候,还是必须保证与get方法的返回值类型一致。

另外,如果set方法的参数没有指定类型,那么会推断为与get方法返回值类型一致。

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

属性索引

类允许定义属性索引。

class MyClass {
  [s:string]: boolean |
    ((s:string) => boolean);

  get(s:string) {
    return this[s] as boolean;
  }
}

上面示例中,[s:string]表示所有属性名类型为字符串的属性,它们的属性值要么是布尔值,要么是返回布尔值的函数。

注意,由于类的方法是一种特殊属性(属性值为函数的属性),所以属性索引的类型定义也涵盖了方法。如果一个对象同时定义了属性索引和方法,那么前者必须包含后者的类型。

class MyClass {
  [s:string]: boolean;
  f() { // 报错
    return true;
  }
}

上面示例中,属性索引的类型里面不包括方法,导致后面的方法f()定义直接报错。正确的写法是下面这样。

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

属性存取器等同于方法,也必须包括在属性索性里面。

class MyClass {
  [s:string]: boolean;

  get(s:string) { // 报错
    return this[s] as boolean;
  }
}

上面示例中,属性索引没有给出方法的类型,导致get()方法报错。正确的写法就是本节一开始的那个例子。

类的 interface 接口

implements 关键字

interface 接口或 type 别名,可以用对象的形式,为 class 指定一组检查条件。然后,类使用 implements 关键字,表示当前类满足这些外部类型条件的限制。

interface Country {
  name:string;
  capital:string;
}
// 或者
type Country = {
  name:string;
  capital:string;
}

class MyCountry implements Country {
  name = '';
  capital = '';
}

上面示例中,interfacetype都可以定义一个对象类型。类MyCountry使用implements关键字,表示该类的实例对象满足这个外部类型。

interface 只是指定检查条件,如果不满足这些条件就会报错。它并不能代替 class 自身的类型声明。

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

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

上面示例中,类B实现了接口A,但是后者并不能代替B的类型声明。因此,Bget()方法的参数s的类型是any,而不是stringB类依然需要声明参数s的类型。

class B implements A {
  get(s:string) {
    return true;
  }
}

下面是另一个例子。

interface A {
  x: number;
  y?: number;
}

class B implements A {
  x = 0;
}

const b = new B();
b.y = 10; // 报错

上面示例中,接口A有一个可选属性y,类B没有声明这个属性,所以可以通过类型检查。但是,如果给B的实例对象的属性y赋值,就会报错。所以,B类还是需要声明可选属性y

class B implements A {
  x = 0;
  y?: number;
}

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

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

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

上面示例中,MyPoint类实现了Point接口,但是内部还定义了一个额外的属性z,这是允许的,表示除了满足接口给出的条件,类还有额外的条件。

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

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

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

上面示例中,implements后面是类Car,这时 TypeScript 就把Car视为一个接口,要求MyCar实现Car里面的每一个属性和方法,否则就会报错。所以,这时不能因为Car类已经实现过一次,而在MyCar类省略属性或方法。

注意,interface 描述的是类的对外接口,也就是实例的公开属性和公开方法,不能定义私有的属性和方法。这是因为 TypeScript 设计者认为,私有属性是类的内部实现,接口作为模板,不应该涉及类的内部代码写法。

interface Foo {
  private member:{}; // 报错
}

上面示例中,接口Foo有一个私有属性,结果就报错了。

实现多个接口

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

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

上面示例中,Car类同时实现了MotorVehicleFlyableSwimmable三个接口。这意味着,它必须部署这三个接口声明的所有属性和方法,满足它们的所有条件。

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

第一种方法是类的继承。

class Car implements MotorVehicle {
}

class SecretCar extends Car implements Flyable, Swimmable {
}

上面示例中,Car类实现了MotorVehicle,而SecretCar类继承了Car类,然后再实现FlyableSwimmable两个接口,相当于SecretCar类同时实现了三个接口。

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

interface A {
  a:number;
}

interface B extends A {
  b:number;
}

上面示例中,接口B继承了接口A,类只要实现接口B,就相当于实现AB两个接口。

前一个例子可以用接口继承改写。

interface MotorVehicle {
  // ...
}
interface Flyable {
  // ...
}
interface Swimmable {
  // ...
}

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

class SecretCar implements SuperCar {
  // ...
}

上面示例中,接口SuperCar通过SuperCar接口,就间接实现了多个接口。

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

interface Flyable {
  foo:number;
}

interface Swimmable {
  foo:string;
}

上面示例中,属性foo在两个接口里面的类型不同,如果同时实现这两个接口,就会报错。

类与接口的合并

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

class A {
  x:number = 1;
}

interface A {
  y:number;
}

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

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

上面示例中,类A与接口A同名,后者会被合并进前者的类型定义。

Class 类型

实例类型

TypeScript 的类本身就是一种类型,但是它代表该类的实例类型,而不是 class 的自身类型。

class Color {
  name:string;

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

const green:Color = new Color('green');

上面示例中,定义了一个类Color。它的类名就代表一种类型,实例对象green就属于该类型。

对于引用实例对象的变量来说,既可以声明类型为 Class,也可以声明类型为 Interface,因为两者都代表实例对象的类型。

interface MotorVehicle {
}

class Car implements MotorVehicle {
}

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

上面示例中,变量c的类型可以写成类Car,也可以写成接口MotorVehicle。它们的区别是,如果类Car有接口MotoVehicle没有的属性和方法,那么只有变量c1可以调用这些属性和方法。

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

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);
}

上面示例中,函数createPoint()的第一个参数PointClass,需要传入 Point 这个类,但是如果把参数的类型写成Point就会报错,因为Point描述的是实例类型,而不是 Class 的自身类型。

由于类名作为类型使用,实际上代表一个对象,因此可以把类看作为对象类型起名。事实上,TypeScript 有三种方法可以为对象类型起名:type、interface 和 class。

类的自身类型

要获得一个类的自身类型,一个简便的方法就是使用 typeof 运算符。

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

上面示例中,createPoint()的第一个参数PointClassPoint类自身,要声明这个参数的类型,简便的方法就是使用typeof Point。因为Point类是一个值,typeof Point返回这个值的类型。注意,createPoint()的返回值类型是Point,代表实例类型。

相关文章

网友评论

      本文标题:TypeScript 详解之 TypeScript 的 clas

      本文链接:https://www.haomeiwen.com/subject/wisnpdtx.html