接口和类

接口(Interface)和类(Class)是TypeScript中的两个核心概念,接口用于定义对象的结构约束,类则用于实现面向对象编程。本篇笔记将详细介绍TypeScript中接口和类的定义与使用方法。

接口

接口(Interface)是TypeScript中用于定义对象结构的一种方式,它描述了对象应该具有哪些属性和方法,但不包含具体实现。接口在编译后会被完全移除,不会产生任何JavaScript代码,它仅用于编译阶段的类型检查。

定义接口

使用interface关键字可以定义一个接口。

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

const user: User = {
  name: "Tom",
  age: 18,
  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
};

上面代码中,我们定义了User接口,它约束了对象必须包含nameage两个属性以及greet()方法。当我们创建user对象时,TypeScript会检查对象结构是否符合接口定义,属性缺失或类型不匹配都会报错。

可选属性和只读属性

接口中可以使用?标记可选属性,使用readonly标记只读属性。

interface User {
  readonly id: number;
  name: string;
  age?: number;
}

const user: User = {
  id: 1,
  name: "Tom",
};

// user.id = 2; // 错误,只读属性不可修改

代码中,id是只读属性,初始化后不可修改;age是可选属性,创建对象时可以省略。

函数类型接口

接口不仅可以描述对象结构,还可以描述函数类型。

interface SearchFunc {
  (source: string, keyword: string): boolean;
}

const search: SearchFunc = (source, keyword) => {
  return source.includes(keyword);
};

不过实际上单纯用接口描述一个函数比较少见,一般描述函数建议使用type声明函数类型表达式,而非用interface来实现,有关函数类型表达式具体可以参考下一章节。

索引签名

当我们不确定对象会有哪些属性,但知道属性的类型规律时,可以使用索引签名。

interface StringMap {
  [key: string]: string;
}

const map: StringMap = {
  name: "Tom",
  city: "Beijing",
};

索引签名支持stringnumber两种类型作为键。

接口继承

接口可以通过extends关键字继承其他接口,且接口支持多继承。

interface Animal {
  name: string;
}

interface Runnable {
  speed: number;
}

interface Dog extends Animal, Runnable {
  breed: string;
}

const dog: Dog = {
  name: "Buddy",
  speed: 20,
  breed: "Labrador",
};

TypeScript中的类是对ES6类语法的增强,增加了访问修饰符、抽象类等特性。

定义类

使用class关键字定义类。

class Person {
  name: string;
  age: number;

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

  greet(): void {
    console.log(`Hello, I'm ${this.name}`);
  }
}

const person = new Person("Tom", 18);
person.greet();

代码中,我们定义了Person类,包含两个属性和一个方法。constructor是构造函数,在创建实例时自动调用。

访问修饰符

TypeScript提供了三种访问修饰符来控制成员的可访问性:

  • public:公开的,任何地方都可以访问(默认)
  • private:私有的,只能在类内部访问
  • protected:受保护的,只能在类内部和子类中访问
class Person {
  public name: string;
  private age: number;
  protected id: number;

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

  private getAge(): number {
    return this.age;
  }
}

const person = new Person("Tom", 18, 1);
console.log(person.name); // 正确
// console.log(person.age); // 错误,私有属性不可访问

参数属性简写

TypeScript提供了一种简写方式,可以在构造函数参数上直接添加访问修饰符,自动创建并初始化类属性,下面两种写法等价。

class Person {
  constructor(
    public name: string,
    private age: number
  ) {}
}
class Person {
  public name: string;
  private age: number;

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

只读属性

使用readonly关键字可以将属性标记为只读,只读属性只能在声明时或构造函数中初始化。

class Person {
  readonly id: number;

  constructor(id: number) {
    this.id = id;
  }
}

const person = new Person(1);
// person.id = 2; // 错误,只读属性不可修改

静态成员

使用static关键字可以定义静态属性和静态方法,静态成员属于类本身而非实例,因此可以通过类名直接调用。

class Counter {
  static count: number = 0;

  static increment(): void {
    Counter.count++;
  }
}

Counter.increment();
console.log(Counter.count); // 1

类的继承

使用extends关键字可以实现类的继承。

class Animal {
  constructor(public name: string) {}

  move(): void {
    console.log(`${this.name} is moving`);
  }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);
  }

  bark(): void {
    console.log("Woof!");
  }

  move(): void {
    console.log(`${this.name} is running`);
  }
}

const dog = new Dog("Buddy", "Labrador");
dog.move(); // Buddy is running
dog.bark(); // Woof!

代码中,Dog类继承了Animal类,通过super()调用父类构造函数,并重写了move()方法。

抽象类

使用abstract关键字可以定义抽象类和抽象方法。抽象类不能被实例化,只能作为基类被继承;抽象方法没有具体实现,必须在子类中实现。

abstract class Shape {
  abstract getArea(): number;

  printArea(): void {
    console.log(`Area: ${this.getArea()}`);
  }
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number
  ) {
    super();
  }

  getArea(): number {
    return this.width * this.height;
  }
}

const rect = new Rectangle(10, 20);
rect.printArea(); // Area: 200

类实现接口

类可以使用implements关键字实现一个或多个接口,这要求类必须实现接口中定义的所有属性和方法。

interface Printable {
  print(): void;
}

interface Loggable {
  log(): void;
}

class Document implements Printable, Loggable {
  print(): void {
    console.log("Printing...");
  }

  log(): void {
    console.log("Logging...");
  }
}

接口实现是TypeScript中实现多态的重要方式,它允许我们定义统一的契约,不同的类可以有不同的实现。

接口和类型别名的区别

在TypeScript中,interfacetype都可以用于定义对象类型,但两者存在一些区别。

// 接口定义
interface User {
  name: string;
}

// 类型别名定义
type UserType = {
  name: string;
};

主要区别如下:

  1. 接口可以重复声明并自动合并,类型别名不可以
  2. 接口只能描述对象结构,类型别名可以描述任意类型
  3. 类只能实现接口,不能实现类型别名
// 接口声明合并
interface User {
  name: string;
}

interface User {
  age: number;
}

// 合并后等价于
interface User {
  name: string;
  age: number;
}

一般来说,描述对象结构优先使用接口,描述联合类型、交叉类型等复杂类型时可使用类型别名。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。