类与接口

类与接口

TypeScript 中的类与其他面向对象的编程语言非常相似,即类是此类(对象)实例所支持的内容的契约定义;此外,类还可以从其他类甚至接口继承功能。不过 TypeScript 中的类仅支持单个构造函数。

interface(接口)

export declare function keys<T extends object>(): Array<keyof T>;

import { keys } from "ts-transformer-keys";

interface Props {
  id: string;
  name: string;
  age: number;
}
const keysOfProps = keys<Props>();

console.log(keysOfProps); // ['id', 'name', 'age']

从 ES6 开始,JavaScript 内建支持使用 class 关键字来声明类,而 TypeScript 允许我们以 implements 来实现某个接口,或者以 extends 关键字来继承某个类:

class Child extends Parent implements IChild, IOtherChild {
  // 类属性
  property: Type;

  // 类属性默认值
  defaultProperty: Type = "default value";

  // 私有属性
  private _privateProperty: Type;

  // 静态属性
  static staticProperty: Type;

  // 构造函数
  constructor(arg1: Type) {
    super(arg1);
  }

  // 私有方法
  private _privateMethod(): Type {}

  methodProperty: (arg1: Type) => ReturnType;

  overloadedMethod(arg1: Type): ReturnType;

  overloadedMethod(arg1: OtherType): ReturnType;

  overloadedMethod(arg1: CommonT): CommonReturnT {}

  // 静态方法
  static staticMethod(): ReturnType {}

  subclassedMethod(arg1: Type): ReturnType {
    super.subclassedMethod(arg1);
  }
}

继承与实现

class TextStory implements Story {
  title: string;
  tags: string[];

  static storyWithNoTags(title: string): TextStory {
    return new TextStory(title, []);
  }

  constructor(title: string, ...tags) {
    this.title = title;
    this.tags = tags;
  }

  summary() {
    return `TextStory ${this.title}`;
  }
}

// 使用静态方法创建类对象
let story = TextStory.storyWithNoTags("Learning TypeScript");

class TutorialStory extends TextStory {
  constructor(title: string, ...tags) {
    // 调用父类构造函数
    super(title, tags);
  }

  // 复写父类的方法
  summary() {
    return `TutorialStory: ${this.title}`;
  }
}

现在 TypeScript 允许我们同时实现多个由 type 或者 interface 声明的类型,并且能够利用交叉操作:

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

interface Shape {
  area(): number;
}

type Perimeter = {
  perimiter(): number;
};

type RectangleShape = Shape & Perimeter & Point;

class Rectangle implements RectangleShape {}

// 等价于
class Rectangle implements Shape, Perimeter, Point {}

在实际项目中,我们往往会去定义公共父类,不过此时要注意父类的公共赋值和子类的默认值之间的冲突。

declare interface ObjectConstructor {
  assign(target: any, ...sources: any[]): any;
}

class A {
  a = 1;

  constructor(data: {} = {}) {
    Object.assign(this, data);
  }
}

class B extends A {
  a = 2;

  // 父类调用 assign 在子类的属性域定义之前
  constructor(data: {} = {}) {
    super(data);
  }
}

// B { a: 2 }
console.log(new B({ a: 3 }));

抽象类

TypeScript 中我们同样可以定义抽象类(Abstract class),即包含抽象方法的类;抽象类不能够被直接初始化,需要通过子类继承并且实现抽象方法。

abstract class StoryProcessorTemplate {
  public process(url: string): Story {
    const title: string = this.extractTitle(url);
    const text: string = this.extractText(url);
    const tags: string[] = this.extractTags(text);
    return {
      title: title,
      tags: tags,
    };
  }

  abstract extractTitle(url: string): string;

  abstract extractText(url: string): string;

  abstract extractTags(url: string): string[];
}

实例:项目管理

Entity

这是一个基类,它定义了其他类将继承的共同特征。

export class Entity {
  private _id: number;
  private _title: string;
  private _creationDate: Date;

  constructor(id: number, title: string) {
    this._id = id;
    this._title = title;
    this._creationDate = new Date();
  }

  get id(): number {
    return this._id;
  }

  get title(): string {
    return this._title;
  }

  set title(title: string) {
    this._title = title;
  }

  get creationDate(): Date {
    return this._creationDate;
  }
}

我们使用 export 关键字开始定义此类。导出类是必不可少的,因此我们可以将其导入其他文件,我们将在其他类的定义中看到。之后,我们定义了三个属性:_id_title_creationDate。使用下划线启动属性对于将它们与访问者(getter 和 setter)区分开来非常重要。在正确定义属性的情况下,我们添加了类的构造函数。此构造函数接受两个参数:一个数字,用于将其用作项目的_id; 和要在_title属性中设置的字符串。构造函数还会自动将_creationDate定义为当前日期。我们在这个类定义中做的最后一件事是添加属性的 getter 和 setter。

Task

import { Entity } from "./entity";

export class Task extends Entity {
  private _completed: boolean;
  private _priority: number;

  get completed(): boolean {
    return this._completed;
  }

  set completed(value: boolean) {
    this._completed = value;
  }

  get priority(): number {
    return this._priority;
  }

  set priority(value: number) {
    this._priority = value;
  }
}

由于此类将从 Entity 继承特性,因此我们通过添加 import 语句来启动此文件以引入 Entity 的定义。之后我们定义 Task 类并使其扩展 Entity。除此之外,这个类没什么特别之处。它只包含两个属性(_completed_priority)及其访问器。请注意,我们没有在 Task 上定义构造函数,因为我们将使用从 Entity 继承的构造函数。

Story

我们将创建的第三个类是 Story,一个代表用户故事的具体类。故事可以细分为多个任务以方便其执行,但只有一个人负责故事及其任务。除此之外,故事包含一个标题(继承自实体)和一个标识故事是否已经完成的标志。要定义 Story 类,让我们使用以下代码在./src 目录中创建一个名为 story.ts 的文件:

import { Entity } from "./entity";
import { Task } from "./task";

export class Story extends Entity {
  private _completed: boolean;
  private _responsible: string;
  private _tasks: Array<Task> = [];

  get completed(): boolean {
    return this._completed;
  }

  set completed(value: boolean) {
    this._completed = value;
  }

  get responsible(): string {
    return this._responsible;
  }

  set responsible(value: string) {
    this._responsible = value;
  }

  public addTask(task: Task) {
    this._tasks.push(task);
  }

  get tasks(): Array<Task> {
    return this._tasks;
  }

  public removeTask(task: Task): void {
    let taskPosition = this._tasks.indexOf(task);
    this._tasks.splice(taskPosition, 1);
  }
}

就像 Task 一样,我们通过扩展 Entity 继承其特性来启动 Story。之后我们定义了三个属性:

  • _completed:标识故事是否已完成的标志。
  • _responsible:一个字符串,用于定义谁负责执行故事及其任务。
  • _tasks:一个包含零个或多个 Task 实例的数组,由负责人执行。

对于前两个属性_completed 和_sponspons,我们定义了两个访问器以启用它们的操作。对于_tasks 属性,我们添加了三个方法。第一个是 addTask,它接受一个 Task 实例来添加到数组中。第二个是获取 Task 的所有实例的访问器。第三个,removeTask,接收一个任务,将其从任务数组中删除。

Project

我们将创建的第四个也是最后一个类将是 Project 类。项目包含零个或多个故事,可以在完成后发布,并且可以具有标题(从实体继承)。要定义此类,让我们在./src 目录中创建一个名为 project.ts 的文件,并添加以下代码:

import { Entity } from "./entity";
import { Story } from "./story";

export class Project extends Entity {
  private _released: boolean;
  private _stories: Array<Story>;

  get released(): boolean {
    return this._released;
  }

  set released(value: boolean) {
    this._released = value;
  }

  public addStory(story: Story) {
    this._stories.push(story);
  }

  get stories(): Array<Story> {
    return this._stories;
  }

  public removeStory(story: Story) {
    let storyPosition = this._stories.indexOf(story);
    this._stories.splice(storyPosition, 1);
  }
}

我们通过导入 Entity 继承其特性来开始 Project 的定义。之后我们定义了两个属性:_released_stories。Project 提供的功能与 Story 非常相似。不同之处在于,项目不是处理一系列任务,而是处理一系列故事。这些故事通过三种方法进行操作:addStory,stories 和 removeStory。这三种方法之间的相似性以及在处理任务的故事中定义的方法之间的相似性很大,因此不需要解释。

Completable

假设我们想要在任务或故事被标记为已完成时触发电子邮件。我们可以定义一个表示可完成项的接口,而不是创建两个不同的函数来单独处理每个类型。为了练习,让我们使用以下源代码在./src 目录中创建一个名为 completable.ts 的文件:

export interface Completable {
  title: string;
  completed: boolean;
  completedAt?: Date;
}

定义此接口后,我们可以使用它来限制哪些对象可以传递给发送电子邮件的函数。让我们在./src 目录中创建一个名为 index.ts 的文件,以查看此操作:

import { Task } from "./task";
import { Completable } from "./completable";

function sendCompletionEmail(completable: Completable) {
  if (!completable.completed) {
    // ignore incompleted entities
    console.error(
      `Please, complete '${completable.title}' before sending email.`
    );
    return;
  }
  console.log(`Sending email about '${completable.title}'`);
  // ...
}

let bugFix = new Task(1, "Weirdo flying bug");
sendCompletionEmail(bugFix);
bugFix.completed = true;
sendCompletionEmail(bugFix);

请注意,TypeScript 不会强制我们显式实现 Completable 接口。编译器在运行时,只需检查传递的对象的结构,看它是否适合接口契约。

// 修改 Task 与 Story 的接口继承
export class Task extends Entity implements Completable {
  // ... nothing else changes here
}

export class Story extends Entity implements Completable {
  // ... nothing else changes here
}
上一页