首页 / TypeScript 入门教程 / 抽象类

TypeScript 入门教程

抽象类

本教程共 80 篇 · 第 34 篇 · 更新于 2026-08-10 · 约 11 分钟阅读

TypeScriptTypeScript 入门教程抽象类abstract模板方法

本节目标:掌握抽象类的定义和使用,理解”模板方法模式”的运作方式,能区分何时用抽象类、何时用接口。

上一章我们用继承来实现代码复用。但有时候,父类本身是不完整的——它定义了”骨架”,却把一些关键步骤留给子类去填充。这种”半成品”就是抽象类。

为什么需要抽象类

假设我们要写一个报表系统。每种报表都需要”获取数据 → 格式化 → 输出”这三个步骤,但不同报表的获取数据和格式化方式完全不同,只有”输出”这一步是共用的。

class PDFReport {
  getData(): string[] {
    return ["PDF数据1", "PDF数据2"];
  }

  format(data: string[]): string {
    return data.join(" | ");
  }

  print(): void {
    const data = this.getData();
    const formatted = this.format(data);
    console.log(`输出 PDF 报表:${formatted}`);
  }
}

class ExcelReport {
  getData(): string[] {
    return ["Excel数据1", "Excel数据2"];
  }

  format(data: string[]): string {
    return data.map((item) => `"${item}"`).join(",");
  }

  print(): void {
    const data = this.getData();
    const formatted = this.format(data);
    console.log(`输出 Excel 报表:${formatted}`);
  }
}

两个类里 print() 方法完全一样,复制粘贴了。正常人会想抽一个父类出来。但问题来了:父类的 getData()format() 该写什么?

这就轮到抽象类登场了。

抽象类的基本语法

abstract 关键字声明抽象类和抽象方法:

abstract class Report {
  abstract getData(): string[];       // 抽象方法:只声明,不实现
  abstract format(data: string[]): string; // 抽象方法:只声明,不实现

  print(): void {                     // 具体方法:有实现
    const data = this.getData();
    const formatted = this.format(data);
    console.log(`输出报表:${formatted}`);
  }
}

抽象方法只有签名,没有方法体。它跟接口里的方法声明很像,但区别在于它活在类里,可以跟具体方法共存。

子类继承抽象类后,必须实现所有抽象方法

class PDFReport extends Report {
  getData(): string[] {
    return ["PDF数据1", "PDF数据2"];
  }

  format(data: string[]): string {
    return data.join(" | ");
  }
}

class ExcelReport extends Report {
  getData(): string[] {
    return ["Excel数据1", "Excel数据2"];
  }

  format(data: string[]): string {
    return data.map((item) => `"${item}"`).join(",");
  }
}

const pdf = new PDFReport();
pdf.print();  // 输出报表:PDF数据1 | PDF数据2

const excel = new ExcelReport();
excel.print(); // 输出报表:"Excel数据1","Excel数据2"

如果子类忘了实现某个抽象方法,TypeScript 会直接报错:

class BrokenReport extends Report {
  // ❌ Non-abstract class 'BrokenReport' does not implement
  //    inherited abstract member 'getData' from class 'Report'.
}

抽象类不能直接实例化

const r = new Report(); // ❌ Cannot create an instance of an abstract class.

这很合理——抽象类本身就是不完整的,直接 new 出来调用抽象方法会炸。

模板方法模式

上面 Report 的例子就是经典的模板方法模式(Template Method Pattern)。抽象类定义算法的骨架(print()),子类负责填充具体步骤(getData()format())。

这个模式在实际开发中非常常见。再举一个更贴近日常的例子——数据校验器:

abstract class Validator {
  validate(value: string): string[] {
    const errors: string[] = [];

    if (!this.required(value)) {
      errors.push("此项为必填");
    }

    const customErrors = this.doValidate(value);
    errors.push(...customErrors);

    return errors;
  }

  // 子类决定是否必填
  protected required(value: string): boolean {
    return true; // 默认必填
  }

  // 子类决定具体的校验逻辑
  protected abstract doValidate(value: string): string[];
}

class EmailValidator extends Validator {
  protected doValidate(value: string): string[] {
    const errors: string[] = [];
    if (!value.includes("@")) {
      errors.push("邮箱格式不正确");
    }
    return errors;
  }
}

class AgeValidator extends Validator {
  protected doValidate(value: string): string[] {
    const errors: string[] = [];
    const age = Number(value);
    if (isNaN(age) || age < 0 || age > 150) {
      errors.push("请输入有效的年龄");
    }
    return errors;
  }
}

const emailValidator = new EmailValidator();
console.log(emailValidator.validate("hello")); // ["邮箱格式不正确"]

const ageValidator = new AgeValidator();
console.log(ageValidator.validate("25"));     // []
console.log(ageValidator.validate("200"));    // ["请输入有效的年龄"]

父类 Validator 定义了校验流程:先检查必填,再执行子类自定义的校验。子类只需要实现 doValidate(),不用关心整个流程。

抽象类作为类型

抽象类不能实例化,但可以当类型用——接收任何子类的实例:

abstract class Shape {
  abstract area(): number;
  abstract perimeter(): number;

  describe(): string {
    return `面积:${this.area().toFixed(2)},周长:${this.perimeter().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }

  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

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

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

  perimeter(): number {
    return 2 * (this.width + this.height);
  }
}

// 抽象类作为类型——可以接收任何子类
function printShapeInfo(shape: Shape): void {
  console.log(shape.describe());
}

printShapeInfo(new Circle(5));
printShapeInfo(new Rectangle(4, 6));

抽象类 vs 接口

这是新手最常纠结的问题之一。两者确实有重叠,但核心区别很清晰:

维度抽象类接口
实例化❌ 不能直接 new❌ 不能直接 new
方法实现✅ 可以有具体方法只能声明(没有方法体)
构造函数✅ 可以有❌ 不能有
访问修饰符✅ public/protected/private❌ 只能 public
属性✅ 可以有初始化值❌ 只能声明类型
继承/实现单继承(extends 一个)多实现(implements 多个)
编译后保留为 JS 类完全消失

选型指南

用抽象类:

  • 你有共享的代码逻辑需要被子类复用(比如 print() 方法)。
  • 你需要 protected 成员,只让子类共享而外部不可见。
  • 你需要构造函数来初始化公共状态。

用接口:

  • 你只需要定义”这个类必须有什么方法/属性”,不需要共享代码。
  • 你需要一个类同时满足多个契约(多实现)。
  • 你想让完全不相关的类实现同一个契约。
Tip

一个实用的判断方法:问问自己”这个基类有没有具体的方法需要被子类直接继承?“如果有,用抽象类;如果没有,用接口。

抽象构造签名

有时候你想写一个函数,接收”任意能产生 Shape 子类的构造函数”。直接写 typeof Shape 会报错——因为 Shape 是抽象的,不能 new

abstract class Shape {
  abstract area(): number;
}

// ❌ 不能直接 new 抽象类
function createShape(ctor: typeof Shape): Shape {
  return new ctor();  // 报错
}

正确的写法是用构造签名 new () => Shape

function createShape(ctor: new () => Shape): Shape {
  return new ctor();
}

class Circle extends Shape {
  area(): number { return 3.14; }
}

createShape(Circle);     // ✅ 没问题
// createShape(Shape);    // ❌ 抽象类不能传进来

new () => Shape 的意思是”一个能用 new 调用、返回 Shape 类型实例的构造函数”。抽象类的构造函数不满足这个签名,所以 TypeScript 会阻止你传入 Shape 本身。

小结

  • abstract class 不能直接 new,只能被继承。
  • abstract method 只有签名没有实现,子类必须实现。
  • 抽象类可以包含具体方法,子类直接继承使用。
  • 模板方法模式是抽象类的经典应用:父类定义骨架,子类填充细节。
  • 抽象类有共享代码时用,只用定义契约时用接口。
  • 抽象类可以当类型使用,接收任意子类实例。
  • 要接收抽象类的子类构造函数,用 new () => Type 构造签名。

抽象类解决的是”继承层次中的公共代码复用”。但有些东西不属于任何实例,而是属于整个类——比如常量、工具函数、实例计数器。那是静态成员的地盘,下一章见。