第 10 章 · 类、装饰器与 this
- 掌握类的字段、构造器、访问修饰符(
public/private/protected/readonly)与静态成员 - 理解抽象类与
implements,知道「类实现接口」的类型含义 - 了解装饰器的两种形态(legacy 与 TC39 提案),看懂它在框架里怎么用
- 掌握
this类型标注,以及ThisType<T>如何驱动「选项式 API」的类型推导 - 通过 Vue / Pinia 案例理解「框架怎么靠类型推导出好用的 API」
10.1 类的基础与访问修饰符
10.1.1 字段、构造器、方法
class Point {
// 字段(field)
x: number
y: number
constructor(x: number, y: number) {
this.x = x
this.y = y
}
distanceTo(other: Point): number {
return Math.hypot(this.x - other.x, this.y - other.y)
}
}
const p = new Point(1, 2)
构造器参数直接声明成字段,用修饰符前缀即可:
class Point {
constructor(
public x: number,
public y: number,
) {}
}
// 等价于上面手写字段 + 赋值,TS 语法糖
10.1.2 访问修饰符
| 修饰符 | 可见范围 |
|---|---|
public(默认) | 任何地方都能访问 |
private | 仅类内部 |
protected | 类内部 + 子类 |
readonly | 只读,初始化后不可改 |
class BankAccount {
private balance: number // 外部碰不到
readonly owner: string // 只读
protected branch: string // 子类可用
constructor(owner: string, branch: string, initial: number) {
this.owner = owner
this.branch = branch
this.balance = initial
}
deposit(amount: number): void {
this.balance += amount
}
getBalance(): number {
return this.balance
}
}
const acc = new BankAccount('ada', '杭州', 100)
acc.balance // ❌ 属性 balance 为私有
acc.owner = 'bob' // ❌ 只读属性
acc.deposit(50) // ✅
private / readonly 都是编译期检查,编译成 JS 后只是普通属性(# 私有字段才是真正的运行时私有)。它们防的是「代码里不该乱碰」,不是「运行时被入侵」。
10.1.3 静态成员
static 成员挂在类上、不属于实例:
class Config {
static version = '1.0.0'
static isProd(): boolean {
return import.meta.env.PROD
}
}
Config.version // '1.0.0'
Config.isProd() // boolean
10.2 抽象类与 implements
10.2.1 抽象类:只定骨架
abstract 方法只声明不实现,交给子类:
abstract class Shape {
abstract area(): number // 子类必须实现
describe(): string {
return `面积为 ${this.area()}`
}
}
class Circle extends Shape {
constructor(private radius: number) {
super()
}
area(): number {
return Math.PI * this.radius ** 2
}
}
new Shape() // ❌ 抽象类不能实例化
const c: Shape = new Circle(2) // ✅ 用抽象类型接实例
10.2.2 implements:类实现接口
implements 要求类满足某个结构(接口或类型):
interface Repository<T> {
find(id: number): T | undefined
save(item: T): void
}
class UserRepo implements Repository<User> {
find(id: number): User | undefined { /* ... */ }
save(item: User): void { /* ... */ }
}
// 漏实现某个方法 → 编译期报错
abstract 类:可以带实现 + 抽象方法,适合「一组相关类的公共骨架」。
implements 接口:纯契约,适合「多个类都满足同一形状」。
两者不互斥,常配合使用。
10.3 装饰器
10.3.1 装饰器是什么
装饰器是「给类/方法/属性/参数附加行为」的语法,形如 @xxx。它在框架里无处不在(Angular、NestJS、TypeORM、Midway 等)。
- legacy 装饰器:TS 传统写法,
experimentalDecorators: true,语法稳定但走「标准化前的旧路」 - TC39 装饰器:新提案,无需
experimentalDecorators,语义略有差异
本书以理解「它在类型层面怎么被推导」为主,语法细节按你项目的配置来。
10.3.2 legacy 装饰器长什么样
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value
descriptor.value = function (...args: unknown[]) {
console.log(`call ${key}`, args)
return original.apply(this, args)
}
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b
}
}
装饰器的类型往往依赖 any——因为「元数据编程」本质是运行时反射。这也是为什么装饰器和 TS 的类型系统关系微妙。
装饰器的语法会随提案演进,别花太多时间背。真正值得掌握的是两件事:① 项目里装饰器开着哪些配置;② 一个「用 ThisType 做选项式 API 推导」的思路(10.5)——那才是类型层面的事。
10.4 this 的类型
10.4.1 给 this 标注类型
回调函数里的 this 是「暗含参数」,可以显式标注:
interface Context {
user: string
}
function greet(this: Context, name: string) {
// 这里 this 是 Context
return `${this.user} 对 ${name} 说你好`
}
greet.call({ user: 'ada' }, 'bob') // ✅
greet('bob') // ❌ 缺少 this 上下文
用 this: void 表示「这个函数不该被作为方法调用」:
function onClick(this: void, e: MouseEvent) {
// 不依赖 this —— 防止被错误绑定
}
10.4.2 ThisType:告诉对象方法「this 是谁」
ThisType<T> 是一个标记类型:把它交叉进某个对象类型,TS 会把这个对象的方法里的 this 推断为 T。它是「选项式 API」类型推导的基石:
// 定义一个「对象字面量选项」的 this 是 D
type Options<D, M> = {
data: D
methods: M & ThisType<D & M> // 关键:methods 里的 this = D & M
}
function create<D, M>(options: Options<D, M>): D & M {
return { ...options.data, ...options.methods }
}
const app = create({
data: { count: 0 },
methods: {
increment() {
this.count++ // ✅ 有类型!this 是 { count: number } & methods
},
},
})
app.increment() // ✅
ThisType 让「对象字面量里的方法能访问兄弟字段」,且全部有类型推导——这是 10.5 框架案例的直接前奏。
10.5 框架案例:Vue 选项式 API 的类型推导
Vue 2 的选项式写法 { data, computed, methods },靠的就是 ThisType 这种思路,让每个区块的 this 拥有「别的区块」的类型:
declare function SimpleVue<D, C, M>(options: {
data: (this: {}) => D // data 里 this 为空(避免误用)
computed: C & ThisType<D> // computed 的 this = data 返回的 D
methods: M & ThisType<D & M & {
[K in keyof C]: C[K] extends (...args: any[]) => any ? ReturnType<C[K]> : never
}> // methods 的 this = D & M & computed 结果
}): any
SimpleVue({
data() {
return { firstname: 'Type', amount: 10 }
},
computed: {
fullname() {
return `${this.firstname}` // ✅ this.firstname 有类型(来自 data)
},
},
methods: {
hi() {
alert(this.fullname) // ✅ this.fullname 有类型(来自 computed)
},
},
})
三行 ThisType 交叉,分别规定了三个区块里 this 能看到什么:
data的this是空对象——防止你在data里误用还没定义好的字段computed的this是D(data 的返回类型)methods的this是D & M & 各 computed 的返回类型
框架的「魔法」= 类型层面的映射 + ThisType。type-challenges 的 00006 SimpleVue / 00213 VueBasicProps / 01290 Pinia 三道 hard 题,本质就是让你亲手写出这种签名。
这三道 hard 题还用到条件类型与 ReturnType(第 7 章)、映射类型(第 6 章)。如果你感觉吃力,先去第 13-14 章把递归和元组练扎实再回来。
本章小结
- 类:字段 / 构造器(含参数属性)/ 修饰符 / 静态成员;
private/readonly是编译期检查 - 抽象类定骨架,
implements定契约 - 装饰器分 legacy 与 TC39 两套,重在「它在框架里怎么用」,别死磕语法
this: T显式标注回调的 this;ThisType<T>驱动选项式 API 推导- Vue / Pinia 的「魔法类型」= 映射 + 条件类型 +
ThisType
本章练习
挑战题(难度较高,建议学完第 4 部分再回做)
| 题号 | 题目 | 难度 | 考察点 |
|---|---|---|---|
| 00006 | 简单的 Vue 类型 | hard | ThisType + 映射 computed 返回 |
| 00213 | Vue Basic Props | hard | props 配置 → 类型 + ThisType |
| 01290 | Pinia | hard | getter/action 的 this 分工 |
| 00017 | 柯里化 1 | hard | 函数式 + infer 递归 |
这 4 道是全书最接近「真实框架类型推导」的题。做完 01290 Pinia,你对「一个 store 的类型怎么被推出来」会豁然开朗。
代码练习
-
访问修饰符:写一个
BankAccount类,用private保护余额、readonly保护账户名、protected给子类用,验证外部访问会报错。 -
抽象类 + implements:定义一个
Animal抽象类(抽象方法speak()),让Dog、Cat实现;再定义一个Flyable接口,让Bird同时extends Animal和implements Flyable。 -
ThisType 选项式:复刻 10.4.2 的
create,给它加一个computed区块(用映射让methods里能访问 computed 的返回值类型)。 -
this 标注:写一个
this: void的回调函数,并写一个this: SomeContext的方法,验证在错误调用方式下会报错。 -
理解 SimpleVue 签名:不看答案,试着为
SimpleVue写一份类型签名(提示:三个泛型 D/C/M + 三处ThisType+ computed 返回映射)。能写出大框架即可,不必一次全对。