TypeScript 进阶指南:抽象类、接口、属性修饰符与泛型
·
从“AnyScript”到工业级代码,这一篇就够了
如果你已经会用 string、number、boolean 等基础类型,恭喜你——你不再是 TypeScript 新手。但当项目规模膨胀到几十万行,你会发现仅仅靠基础类型远远不够。我们需要更强大的抽象能力、更严格的访问控制、可复用的“类型逻辑”。
本文将带你掌握四大核心武器:
- 抽象类 —— 定义骨架,强制子类填空
- 接口 —— 契约式编程的基石
- 属性修饰符 —— 让类的内部真正“私有”
- 泛型 —— 一段代码,适配所有类型
读完本文,你将写出既灵活又安全的 TypeScript 代码,并且能直接在组件开发中落地。
1. 抽象类:当普通类不够“抽象”时
- 为什么需要抽象类?
- 假设你正在写一个游戏:所有角色(Character)都能 attack(),但攻击方式各不相同;都能 move(),但移动逻辑也可能有差异。你希望定义一套统一规范,同时提供一些通用实现。这时候抽象类就是最合适的工具。
- 语法和特点
- 用 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
- 什么时候使用?
- 多个类拥有相同的基类逻辑(如公共字段、公共方法),但部分行为必须由子类自定义。
- 你想提供一个“模板方法”模式:基类控制流程,子类实现细节。
2. 接口:比抽象类更纯粹的“契约”
- 对比
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 实例化 | 不可实现 | 不可实现 |
| 方法实现 | 有具体的 | 只能有方法签名 |
| 字段 | 有字段实例 | 有(只读或可选) |
| 多继承 | 只能继承一个类 | 可以实现多个接口 |
- 核心用法
- 接口描述一个对象应该具有哪些属性/方法,但不关心具体实现。
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;
}
}
- 可选属性和只读属性
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"; // 可以赋值可选属性
- 函数类型接口
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. 泛型:让组件“通吃”所有类型
- 泛型解决什么问题?
假设你要写一个工具函数,返回数组的第一个元素。如果不使用泛型,你可能要用 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
- 基本语法
- <T> 声明一个类型变量,可以命名为任何合法标识符(常用 T、U、K、V)。
- 使用时可以显式指定类型参数,也可以利用类型推断(推荐)。
// 显式指定
let result = firstElement<boolean>([true, false]);
- 泛型约束
- 有时候你需要泛型具有某些属性,比如要求传入的对象必须有 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 属性
- 泛型类
- 类也可以使用泛型,例如一个通用栈:
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");
- 泛型接口
- 接口也可以是泛型的,例如定义一个通用的响应格式:
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! 🚀
更多推荐
所有评论(0)