TypeScript 中的类
给面向对象编程加上类型保护
🎯 学习目标
学完本章,你将能够:
- 能够使用 public、private、protected、readonly 修饰类成员
- 能够定义抽象类与抽象方法约束子类行为
- 能够使用 implements 实现接口约束与 override 安全重写
- 能够理解 Mixin 模式实现多重行为组合
- 能够运用类装饰器、方法装饰器和属性装饰器扩展元数据
先看一个问题
JavaScript 的 class 没有类型检查,属性可以随便加、随便改。如何让 class 也享受类型安全?
// JavaScript:一切都很自由,自由到危险
class User {
constructor(name, age) {
this.name = name;
// 忘记赋值 this.age?不会报错
}
}
const user = new User("张三", 25);
user.namme = "李四"; // 拼写错误,不会报错
user.age = "二十六"; // 类型错误,不会报错
TypeScript 为 class 添加了完整的类型注解能力:属性必须声明、方法有返回类型、访问修饰符控制可见性。
Part 1: 属性类型
属性必须先声明类型,才能在构造器中赋值
JavaScript 的 class 随便加属性,拼写错了也不报错。TypeScript 要求你在类体中先声明每个属性的类型——漏了就报错。这看似麻烦,实则帮你在编译期挡住了大量低级错误。我们从最基础的属性声明和构造器开始。
属性声明
在类体中声明,构造器中初始化
class Person {
name: string; // 必须声明类型
age: number; // 必须声明类型
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `你好,我是${this.name},今年${this.age}岁`;
}
}
const p = new Person("张三", 25);
// strictPropertyInitialization 开启后
// 属性必须在构造器中初始化,或者声明为可选
class Config {
host: string; // 必须在 constructor 中赋值
port?: number; // 可选属性,可以不初始化
timeout: number;
constructor(host: string, timeout: number) {
this.host = host;
this.timeout = timeout;
}
}
// 确定赋值断言(当你知道属性会在外部初始化)
class App {
private instance!: HTMLElement; // ! 告诉 TS 一定会被赋值
mount() {
this.instance = document.getElementById("app")!;
}
}
Part 2: 访问控制
public、private、protected、readonly——控制谁能访问什么
属性声明好了,但谁有权访问?密码字段不该暴露给外部,角色信息子类要用但外部不该碰。四个访问修饰符让你精确控制每个属性和方法的可见性。再加上 getter/setter 和参数属性的简写,类的设计既安全又简洁。
四个修饰符
控制属性和方法的可见性
class User {
public name: string; // 公开(默认)
private password: string; // 仅类内部
protected role: string; // 类内部 + 子类
readonly id: string; // 只读
constructor(name: string, password: string, role: string) {
this.id = crypto.randomUUID();
this.name = name;
this.password = password;
this.role = role;
}
}
const user = new User("张三", "secret", "admin");
console.log(user.name); // OK
// console.log(user.password); // 报错!private
// console.log(user.role); // 报错!protected
| 修饰符 | 类内部 | 子类 | 外部 |
|---|---|---|---|
public | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 不可以 |
private | 可以 | 不可以 | 不可以 |

图:四种访问修饰符的可见性范围
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 同心圆靶心图:内圈类自身(深蓝)→中圈子类(蓝)→外圈外部(浅蓝) - 每圈标注:public✓✓✓ | protected✓✓✗ | private✓✗✗ | #✓✗✗ - 底部:"#是ECMAScript标准,private是TS编译时检查" - 配色:从内到外深蓝→浅蓝渐变,不可访问红色叉号,16:9
getter 与 setter
用属性的方式访问,用方法的方式控制
class User {
private _name: string;
constructor(name: string) {
this._name = name;
}
// getter——读取时自动调用
get displayName(): string {
return `@${this._name}`;
}
// setter——赋值时自动调用
set displayName(val: string) {
this._name = val.replace("@", "");
}
}
const user = new User("张三");
console.log(user.displayName); // "@张三"(调用 getter)
user.displayName = "@李四"; // 调用 setter
console.log(user.displayName); // "@李四"
getter/setter 看起来像属性访问,实际是方法调用。适合在读写时加入验证逻辑或计算逻辑。
参数属性
在构造器参数前加修饰符,自动创建属性
// 参数属性——构造器参数前加修饰符
class User {
constructor(
public name: string, // 自动创建 public name
private password: string, // 自动创建 private password
protected role: string, // 自动创建 protected role
readonly id: string = crypto.randomUUID(),
) {
// 无需手动 this.name = name 等
}
}
// 等同于手写版本(但更简洁)
class UserVerbose {
public name: string;
private password: string;
protected role: string;
readonly id: string;
constructor(name: string, password: string, role: string) {
this.name = name;
this.password = password;
this.role = role;
this.id = crypto.randomUUID();
}
}
Part 3: 静态成员
static 让属性和方法属于类本身,而不是实例
有些东西不该属于某个实例,而应该属于类本身——比如工具方法、版本号、单例实例。static 修饰的成员不需要 new 就能访问,直接通过类名调用。单例模式、工厂方法、静态初始化块都依赖它。
static 基础
属于类本身,不属于实例
class MathUtils {
static PI = 3.14159;
static readonly VERSION = "1.0"; // static + readonly = 常量
static circleArea(radius: number): number {
return MathUtils.PI * radius * radius;
}
}
// 通过类名访问,不需要 new
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.circleArea(5)); // 78.53975
// const m = new MathUtils();
// m.PI; // 报错!static 成员不在实例上
ChildClass.staticMethod() 访问父类的 static 成员,但它们挂载在类(构造函数)对象上,不在 prototype 链中。
static 进阶
单例模式与 static 初始化块
// 单例模式——private constructor + static getInstance
class UserService {
private static instance: UserService | null = null;
static readonly serviceName = "UserService";
private constructor() {} // 禁止外部 new
static getInstance(): UserService {
if (!UserService.instance) {
UserService.instance = new UserService();
}
return UserService.instance;
}
}
const svc1 = UserService.getInstance();
const svc2 = UserService.getInstance();
console.log(svc1 === svc2); // true——永远是同一个实例
// static + 泛型——工厂方法
class Entity {
static create<T extends Entity>(this: new () => T): T {
return new this();
}
}
// static 初始化块(ES2022+)——复杂静态逻辑
class Config {
static readonly DB_URL: string;
static readonly PORT: number;
static {
// 类加载时执行一次
Config.DB_URL = process.env.DB_URL ?? "localhost:5432";
Config.PORT = parseInt(process.env.PORT ?? "3000");
}
}
Part 4: 抽象与实现
abstract class 和 implements——类的设计模式
多个子类共享同一段逻辑,但又各有各的实现细节——怎么设计?抽象类定义通用行为,子类负责填充具体实现。配合 override 关键字,方法重写更安全;配合 implements,类可以同时满足多个接口的约束。这是面向对象设计的核心工具。
抽象类
不能实例化,只能被继承的基类
// 抽象类——定义通用行为,强制子类实现细节
abstract class Animal {
constructor(public name: string) {}
// 抽象方法——子类必须实现
abstract makeSound(): string;
// 具体方法——子类直接继承
describe(): string {
return `${this.name}说: ${this.makeSound()}`;
}
}
// const a = new Animal("动物"); // 报错!不能实例化抽象类
class Dog extends Animal {
makeSound(): string { return "汪汪!"; }
}
class Cat extends Animal {
makeSound(): string { return "喵~"; }
}
const dog = new Dog("旺财");
console.log(dog.describe()); // "旺财说: 汪汪!"
抽象属性
抽象类不仅能约束方法,还能约束属性
interface User { id: string; name: string; email: string; }
abstract class Repository<T> {
abstract tableName: string; // 子类必须声明表名
abstract primaryKey: string; // 子类必须声明主键
abstract findById(id: string): Promise<T>;
// 具体方法可以使用抽象属性
async query(sql: string): Promise<T[]> {
console.log(`Querying ${this.tableName}...`);
return [] as T[];
}
}
class UserRepository extends Repository<User> {
tableName = "users";
primaryKey = "id";
async findById(id: string): Promise<User> {
return this.query(
`SELECT * FROM ${this.tableName} WHERE ${this.primaryKey} = '${id}'`
).then(rows => rows[0]);
}
}
抽象属性强制子类提供配置信息,配合具体方法实现"模板方法"模式——基类定流程,子类填细节。
override 关键字
TS 4.3+ 显式标记方法重写
class Animal {
move(): void { console.log("moving"); }
eat(): void { console.log("eating"); }
}
class Dog extends Animal {
override move(): void { // OK——move 存在于基类
console.log("running");
}
override bark(): void { // 报错!bark 不存在于 Animal
console.log("woof");
}
}
// tsconfig.json 中开启 noImplicitOverride
// {
// "compilerOptions": {
// "noImplicitOverride": true
// }
// }
// 开启后,所有重写父类的方法都必须加 override
// 为什么用 override?
// 1. 防止拼写错误:wriet() 不会静默创建新方法
// 2. 基类改名/删除方法时,子类立刻报错
// 3. 代码意图更清晰——一眼就知道这是重写
override。配合 noImplicitOverride 可以完全杜绝"方法名拼写错误"导致的隐蔽 bug。
implements 实现
类承诺满足接口的形状
// implements——类实现接口
interface Serializable {
serialize(): string;
}
interface Loggable {
log(): void;
}
// 一个类可以实现多个接口
class UserEntity implements Serializable, Loggable {
constructor(public name: string, public age: number) {}
serialize(): string {
return JSON.stringify({ name: this.name, age: this.age });
}
log(): void {
console.log(this.serialize());
}
}
| 关键词 | 作用 | 是否获得实现 |
|---|---|---|
extends | 继承另一个类 | 是(获得代码复用) |
implements | 实现接口 | 否(纯类型约束) |
Part 5: 泛型类
让类也能参数化类型,适配不同数据
第28章学了泛型函数和泛型接口,泛型类是拼图的最后一块。数据容器、Repository 模式、Mixin 组合——这些都离不开泛型类。它让类像函数一样可以"传类型参数",一个 DataStore 类就能同时服务 User、Post、Order。
泛型数据容器
一个类适配多种数据类型
// 泛型数据容器
class DataStore<T> {
private items: T[] = [];
add(item: T): void { this.items.push(item); }
get(index: number): T | undefined { return this.items[index]; }
getAll(): T[] { return [...this.items]; }
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
get count(): number { return this.items.length; }
}
const userStore = new DataStore<{ name: string; age: number }>();
userStore.add({ name: "张三", age: 25 });
userStore.add({ name: "李四", age: 30 });
userStore.find(u => u.name === "张三"); // 找到了
泛型类 + 约束
限制泛型参数必须满足某个接口
// 泛型类 + 接口约束
interface Identifiable { id: string; }
class Repository<T extends Identifiable> {
private items = new Map<string, T>();
add(item: T): void {
this.items.set(item.id, item);
}
findById(id: string): T | undefined {
return this.items.get(id);
}
remove(id: string): boolean {
return this.items.delete(id);
}
}
interface Post extends Identifiable {
id: string;
title: string;
content: string;
}
const postRepo = new Repository<Post>();
postRepo.add({ id: "p1", title: "第一篇", content: "Hello" });
postRepo.findById("p1"); // Post | undefined
Mixin 模式
用函数组合类的功能,替代多继承
// TypeScript 不支持多继承(extends 只能一个)
// Mixin 模式:用函数给类"混入"额外功能
// 时间戳 Mixin
function Timestamped<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
};
}
// 软删除 Mixin
function SoftDeletable<T extends new (...args: any[]) => {}>(Base: T) {
return class extends Base {
deletedAt: Date | null = null;
softDelete(): void { this.deletedAt = new Date(); }
get isDeleted(): boolean { return this.deletedAt !== null; }
};
}
// 组合使用——比继承更灵活
class Article {
constructor(public title: string, public content: string) {}
}
const EnhancedArticle = SoftDeletable(Timestamped(Article));
const article = new EnhancedArticle("标题", "内容");
article.softDelete();
console.log(article.createdAt); // Date
console.log(article.isDeleted); // true
Mixin 的核心思想:每个 Mixin 是一个高阶函数,接收一个类,返回一个扩展后的新类。组合顺序不影响功能,但影响类型推断。
继承与实现
extends 和 implements 有什么本质区别?

图:extends 与 implements 的区别
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:左右对比布局 - 左侧 extends(类继承):父类 Animal 有属性 name 和方法 makeSound(),子类 Dog extends Animal 自动获得 name 和 makeSound() 的实现,绿色箭头标注"获得实现代码" - 右侧 implements(接口实现):接口 Serializable 声明了 serialize() 方法签名(只有声明没有实现),类 UserEntity implements Serializable 必须自己实现 serialize() 方法,橙色箭头标注"只获得类型约束" - 底部对比:"extends = 拿代码 | implements = 拿契约" - 配色建议:浅灰背景,extends 绿色系,implements 橙色系 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
简单记忆:extends 拿到的是代码(实现),implements 拿到的是契约(约束)。一个类只能 extends 一个父类,但可以 implements 多个接口。
类表达式
class 不只能声明,还能当值用
// 匿名类赋值给变量
const MyComponent = class {
render(): string { return "<div>Hello</div>"; }
};
const comp = new MyComponent();
console.log(comp.render()); // "<div>Hello</div>"
// 类表达式配合接口
interface StorageLike<T> {
get(): T;
set(value: T): void;
}
// 工厂函数返回类
const createStorage = <T>(initial: T): StorageLike<T> => {
return class {
private data: T = initial;
get(): T { return this.data; }
set(value: T): void { this.data = value; }
};
};
const numStorage = createStorage(42);
numStorage.set(100);
console.log(numStorage.get()); // 100
类表达式常见于工厂模式——根据参数动态创建不同配置的类。也常用于 Mixin 模式的返回值(前面讲的 Timestamped 就是类表达式)。
Part 6: 装饰器
用函数"装饰"类和方法——Angular/NestJS 的核心机制
如果你用过 Angular 或 NestJS,一定见过 @Component、@Get 这样的写法。装饰器就是在不修改原有代码的情况下,给类和方法"加料"。它是 JavaScript TC39 Stage 3 提案,属于进阶内容,但在框架开发中几乎绕不开。
类装饰器
用一个函数包装整个类
// 类装饰器——接收类的构造函数
function Log(target: Function) {
console.log(`类已创建: ${target.name}`);
}
@Log
class MyClass {}
// 实例化时不会触发,类定义时触发
// 控制台输出: "类已创建: MyClass"
// 带参数的装饰器——装饰器工厂
function Version(version: string) {
return function (target: Function) {
(target as any).version = version;
};
}
@Version("2.0.0")
class API {}
console.log((API as any).version); // "2.0.0"
tsconfig.json 中开启 "experimentalDecorators": true。TC39 Stage 3 新提案的语法有所不同,但目前主流框架(Angular、NestJS)仍在使用"实验性装饰器"。
方法装饰器
增强或拦截类的方法
// 方法装饰器——拦截方法调用
function Deprecated(message: string) {
return function (
target: any,
key: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.warn(`警告: ${key} 已弃用。${message}`);
return original.apply(this, args);
};
};
}
// 只读装饰器——防止方法被覆盖
function readonly(
target: any,
key: string,
descriptor: PropertyDescriptor
) {
descriptor.writable = false;
}
class APIService {
@Deprecated("请使用 fetchUsers 替代")
getUsers(): string[] { return ["张三", "李四"]; }
@readonly
getConfig(): object { return { version: "1.0" }; }
}
const api = new APIService();
api.getUsers(); // 控制台警告: "getUsers 已弃用。请使用 fetchUsers 替代"
属性装饰器
拦截属性的声明,添加元数据
// 用到 Reflect 元数据的装饰器,需要先安装并引入 polyfill:
// $ npm i reflect-metadata
// import "reflect-metadata"; // 在入口文件顶部引入一次即可
// 并在 tsconfig 开启:"emitDecoratorMetadata": true, "experimentalDecorators": true
// 属性装饰器——监听属性声明
function Range(min: number, max: number) {
return function (target: any, key: string) {
// 将验证规则存入元数据
const metaKey = `__range_${key}__`;
Object.defineProperty(target, metaKey, {
value: { min, max },
enumerable: false,
});
};
}
function Required(target: any, key: string) {
const metaKey = `__required__`;
// Reflect.getMetadata 来自 reflect-metadata(标准 Reflect 只有 get,没有 getMetadata)
const existing: string[] = Reflect.getMetadata
? Reflect.getMetadata(metaKey, target) || []
: (target[metaKey] || []);
existing.push(key);
}
class UserForm {
@Range(0, 150)
age: number = 0;
@Required
name: string = "";
}
// 装饰器在类定义时执行,不是实例化时
// 常用于表单验证、ORM 映射、依赖注入
属性装饰器无法直接修改属性值,但可以通过 Object.defineProperty 或元数据(Reflect.metadata)记录信息,供运行时框架读取。
装饰器实战
Angular / NestJS / TypeORM 中的装饰器
// Angular——组件装饰器
@Component({
selector: "app-root",
template: "<h1>{{ title }}</h1>",
})
class AppComponent {
@Input() title = "";
}
// NestJS——路由 + 依赖注入
@Controller("users")
class UserController {
@Get(":id")
findOne(@Param("id") id: string) { /* ... */ }
}
// TypeORM——实体映射
@Entity("users")
class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
name: string;
@CreateDateColumn()
createdAt: Date;
}
| 框架 | 装饰器类型 | 核心用途 |
|---|---|---|
| Angular | @Component, @Input, @Output | 组件元数据与绑定 |
| NestJS | @Controller, @Get, @Injectable | 路由与依赖注入 |
| TypeORM | @Entity, @Column, @OneToOne | 数据库表映射 |
tsconfig.json 开启 "experimentalDecorators": true 和 "emitDecoratorMetadata": true(后者用于 NestJS 依赖注入的类型推断)。
类 vs 函数式
何时用 class,何时用普通函数?
// 函数式风格——简单、组合方便
type User = { id: string; name: string; age: number };
const createUser = (name: string, age: number): User => ({
id: crypto.randomUUID(), name, age,
});
const greet = (user: User): string => `你好,${user.name}`;
// OOP 风格——封装状态、行为与数据绑定
class UserService {
private users = new Map<string, User>();
add(user: User): void { this.users.set(user.id, user); }
find(id: string): User | undefined { return this.users.get(id); }
}
| 场景 | 推荐 | 原因 |
|---|---|---|
| 纯数据变换 | 函数 | 输入 → 输出,无副作用 |
| 有状态的服务 | class | 封装内部状态,方法操作数据 |
| React 组件 | 函数 | Hooks 时代,函数组件为主 |
| Node.js 后端 | 看情况 | Service 用 class,工具用函数 |
链表练习
练习
任务:实现一个类型安全的链表类 LinkedList<T>,包含 add、remove、find、toArray 方法。
// 提示:需要一个内部 Node 类
class ListNode<T> {
constructor(
public value: T,
public next: ListNode<T> | null = null
) {}
}
// 你的代码:
class LinkedList<T> {
private head: ListNode<T> | null = null;
private _size = 0;
// add(item: T): void
// remove(item: T): boolean
// find(predicate: (item: T) => boolean): T | undefined
// toArray(): T[]
// get size(): number
}
参考答案
class LinkedList<T> {
private head: ListNode<T> | null = null;
private _size = 0;
add(item: T): void {
const node = new ListNode(item);
if (!this.head) { this.head = node; }
else {
let curr = this.head;
while (curr.next) curr = curr.next;
curr.next = node;
}
this._size++;
}
remove(item: T): boolean {
if (!this.head) return false;
if (this.head.value === item) {
this.head = this.head.next;
this._size--; return true;
}
let curr = this.head;
while (curr.next) {
if (curr.next.value === item) {
curr.next = curr.next.next;
this._size--; return true;
}
curr = curr.next;
}
return false;
}
find(predicate: (item: T) => boolean): T | undefined {
let curr = this.head;
while (curr) {
if (predicate(curr.value)) return curr.value;
curr = curr.next;
}
return undefined;
}
toArray(): T[] {
const result: T[] = [];
let curr = this.head;
while (curr) { result.push(curr.value); curr = curr.next; }
return result;
}
get size(): number { return this._size; }
}
常见问题
问题一:属性未初始化报错
class App {
name: string; // 报错!属性没有初始化
}
// 解决方案(三选一)
class App1 { name: string = ""; } // 默认值
class App2 { name!: string; } // 确定赋值断言
class App3 { name?: string; } // 可选属性
问题二:private 和 # 的区别
class A {
private x = 1; // TS 编译期的 private,编译后属性名仍是 x
#y = 2; // ES 真正的 private,运行时也安全
}
// private 只是编译期检查,编译后产物里仍是 this.x,运行时可被访问
// # 是 ECMAScript 标准,运行时也完全不可访问
建议:新项目优先使用 # 真正的私有字段。
this 绑定
类方法中的 this 丢失问题
class Timer {
private seconds = 0;
// 问题:普通函数中 this 指向不正确
startBad() {
setInterval(function () {
this.seconds++; // 报错!this 不是 Timer
}, 1000);
}
// 修复 1:箭头函数自动绑定外层 this
startGood() {
setInterval(() => {
this.seconds++; // OK——箭头函数没有自己的 this
}, 1000);
}
// 修复 2:箭头函数属性(class field)——永久绑定
tick = () => {
this.seconds++;
};
}
const timer = new Timer();
// 回调场景:方法作为参数传递时 this 可能丢失
setTimeout(timer.tick, 1000); // OK——tick 是箭头函数,已绑定
this 丢失。解决方式:箭头函数属性(推荐)、.bind(this)、或在回调中使用箭头函数包裹。
试一试
练习一
任务:定义一个 BankAccount 类,有 owner(公开)、balance(私有)属性,以及 deposit 和 withdraw 方法。
// owner: string(公开)
// balance: number(私有,初始为 0)
// deposit(amount: number): void
// withdraw(amount: number): boolean
// getBalance(): number
// 你的代码:
参考答案
class BankAccount {
constructor(public owner: string) {}
private balance: number = 0;
deposit(amount: number): void {
if (amount <= 0) throw new Error("金额必须大于0");
this.balance += amount;
}
withdraw(amount: number): boolean {
if (amount > this.balance) return false;
this.balance -= amount;
return true;
}
getBalance(): number {
return this.balance;
}
}
练习二
任务:用参数属性重写上面的 BankAccount,同时定义一个抽象类 Account,让 BankAccount 继承它。
// abstract class Account { ... }
// class BankAccount extends Account { ... }
// 你的代码:
参考答案
abstract class Account {
abstract getBalance(): number;
abstract deposit(amount: number): void;
}
class BankAccount extends Account {
constructor(public owner: string, private balance: number = 0) {
super();
}
getBalance(): number { return this.balance; }
deposit(amount: number): void { this.balance += amount; }
}
📝 要点总结
本章核心知识点
修饰符
public/private/protected 访问控制,readonly 只读
抽象与继承
abstract 抽象类/方法,extends 继承,override 安全重写
接口实现
implements 约束类结构,多接口实现
Mixin 模式
通过函数组合实现多重行为继承
装饰器
类/方法/属性装饰器,元数据反射,实验性特性
下一章
TypeScript 的类让 OOP 更安全。下一章我们学习模块与声明文件——让类型在项目之间传递。