从“AnyScript”到工业级代码,这一篇就够了

如果你已经会用 string、number、boolean 等基础类型,恭喜你——你不再是 TypeScript 新手。但当项目规模膨胀到几十万行,你会发现仅仅靠基础类型远远不够。我们需要更强大的抽象能力、更严格的访问控制、可复用的“类型逻辑”。

本文将带你掌握四大核心武器:

  • 抽象类 —— 定义骨架,强制子类填空
  • 接口 —— 契约式编程的基石
  • 属性修饰符 —— 让类的内部真正“私有”
  • 泛型 —— 一段代码,适配所有类型

读完本文,你将写出既灵活又安全的 TypeScript 代码,并且能直接在组件开发中落地。

1. 抽象类:当普通类不够“抽象”时

  1. 为什么需要抽象类?
  • 假设你正在写一个游戏:所有角色(Character)都能 attack(),但攻击方式各不相同;都能 move(),但移动逻辑也可能有差异。你希望定义一套统一规范,同时提供一些通用实现。这时候抽象类就是最合适的工具。
  1. 语法和特点
  • 用 abstract 关键字修饰类和方法。
  • 抽象方法只有签名,没有实现体。
  • 抽象类不能直接实例化,必须被子类继承并实现所有抽象方法。
abstract class Character {
  constructor(public name: string) {}

  // 抽象方法:子类必须实现
  abstract attack(): void;

  // 普通方法:子类可以直接继承
  move(): void {
    console.log(`${this.name} moves forward`);
  }
}

class Warrior extends Character {
  attack(): void {
    console.log(`${this.name} swings a sword!`);
  }
}

class Mage extends Character {
  attack(): void {
    console.log(`${this.name} casts a fireball!`);
  }
}

// const char = new Character("Test"); // 错误:无法创建抽象类的实例
const warrior = new Warrior("Arthur");
warrior.attack(); // Arthur swings a sword!
warrior.move();   // Arthur moves forward
  1. 什么时候使用?
  • 多个类拥有相同的基类逻辑(如公共字段、公共方法),但部分行为必须由子类自定义。
  • 你想提供一个“模板方法”模式:基类控制流程,子类实现细节。

2. 接口:比抽象类更纯粹的“契约”

  1. 对比
特性 抽象类 接口
实例化 不可实现 不可实现
方法实现 有具体的 只能有方法签名
字段 有字段实例 有(只读或可选)
多继承 只能继承一个类 可以实现多个接口
  1. 核心用法
  • 接口描述一个对象应该具有哪些属性/方法,但不关心具体实现。
interface Drawable {
  color: string;
  draw(): void;
}

interface Resizable {
  resize(percent: number): void;
}

class Circle implements Drawable, Resizable {
  constructor(public color: string, public radius: number) {}

  draw(): void {
    console.log(`Drawing a ${this.color} circle`);
  }

  resize(percent: number): void {
    this.radius *= percent;
  }
}
  1. 可选属性和只读属性
interface User {
  readonly id: number;    // 只读,不可修改
  name: string;
  email?: string;         // 可选
}

const u: User = { id: 1, name: "Alice" };
u.id = 2;   // 错误:id 是只读的
u.email = "alice@example.com"; // 可以赋值可选属性
  1. 函数类型接口
interface StringTransformer {
  (input: string): string;
}

const toUpperCase: StringTransformer = (s) => s.toUpperCase();

小技巧:优先使用接口来描述对象的形状(shape),直到你需要构造函数或具体实现时再考虑抽象类。

3. 属性修饰符:管好你的类内部状态

TypeScript 提供了 public、private、protected、readonly 以及参数属性简写,让你像 Java/C# 一样控制访问权限。

修饰符 类内部可访问 子类可访问 实例外部可访问
public 可以 可以 可以
private 可以 不可以 不可以
protected 可以 可以 不可以
readonly 可以(仅初始化) 可以(仅读取) 可以(仅读取)
  • 示例:员工类的封装
class Employee {
  public name: string;          // 公开
  private salary: number;       // 私有,外部无法访问
  protected department: string; // 受保护,子类可访问
  readonly hireDate: Date;      // 只读

  constructor(name: string, salary: number, department: string, hireDate: Date) {
    this.name = name;
    this.salary = salary;
    this.department = department;
    this.hireDate = hireDate;
  }

  public getSalary(): number {
    return this.salary; // 类内部可以访问私有属性
  }
}

class Manager extends Employee {
  getDepartment(): string {
    return this.department; // 子类可以访问 protected
  }
}

const emp = new Employee("John", 5000, "IT", new Date());
emp.name = "Johnny";        // 允许
emp.salary = 6000;          // 错误:属性“salary”为私有
emp.hireDate = new Date();  // 错误:只读属性不能修改
  • 参数属性
    • 直接在构造函数参数前加上修饰符,会自动创建并初始化同名字段。
class Animal {
  constructor(
    public name: string,
    private age: number,
    protected species: string
  ) {}
}

// 等价于:
// class Animal {
//   public name: string;
//   private age: number;
//   protected species: string;
//   constructor(name: string, age: number, species: string) {
//     this.name = name; this.age = age; this.species = species;
//   }
// }

4. 泛型:让组件“通吃”所有类型

  1. 泛型解决什么问题?
    假设你要写一个工具函数,返回数组的第一个元素。如果不使用泛型,你可能要用 any,但这样会丢失类型信息:
function firstElement(arr: any[]): any {
  return arr[0];
}
const num = firstElement([1, 2, 3]); // num 类型是 any,得不到数字保护

使用泛型,你可以保留传入的类型,同时返回值也自动推断为该类型:

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}
const num = firstElement([1, 2, 3]); // num 推断为 number | undefined
const str = firstElement(["a", "b"]); // str 推断为 string | undefined
  1. 基本语法
  • <T> 声明一个类型变量,可以命名为任何合法标识符(常用 T、U、K、V)。
  • 使用时可以显式指定类型参数,也可以利用类型推断(推荐)。
// 显式指定
let result = firstElement<boolean>([true, false]);
  1. 泛型约束
  • 有时候你需要泛型具有某些属性,比如要求传入的对象必须有 length 属性。这时使用 extends 进行约束。
interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello");     // ✅ string 有 length
logLength([1, 2, 3]);   // ✅ 数组有 length
logLength(123);         // ❌ number 没有 length 属性
  1. 泛型类
  • 类也可以使用泛型,例如一个通用栈:
class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
}

const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20

const stringStack = new Stack<string>();
stringStack.push("TypeScript");
  1. 泛型接口
  • 接口也可以是泛型的,例如定义一个通用的响应格式:
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

type UserData = { id: number; name: string };
type UserResponse = ApiResponse<UserData>;

const res: UserResponse = {
  code: 200,
  message: "success",
  data: { id: 1, name: "Alice" }
};

5. 结束语:从“会用”到“会设计”

恭喜你,你已经掌握了 TypeScript 进阶的四大核心武器。现在回顾一下:

  • 抽象类 —— 定义蓝图,强制子类实现细节
  • 接口 —— 轻量级契约,支持多实现
  • 属性修饰符 —— 让你的代码更封装、更安全
  • 泛型 —— 写出真正可复用的组件,告别 any

这些特性单独使用已经很有威力,而当你把它们组合起来时——例如用泛型接口描述 API 数据,用抽象类定义服务基类,再用 private/protected 控制状态——你就能构建出健壮、可扩展、易于维护的应用架构。

学习 TypeScript 就像升级装备:基础类型是木剑,而抽象、泛型、接口等就是钻石剑。不要满足于让代码“能跑”,要去追求让代码“难以误用”。

现在,打开你的编辑器,把这些概念应用到你的下一个模块中吧。如果你有任何疑问或想深入探讨某个话题,欢迎在评论区留言。

Happy Typing! 🚀

更多推荐