跳到主要内容

第 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) // ✅
编译期 vs 运行期

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 vs implements

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 能看到什么:

  1. datathis 是空对象——防止你在 data 里误用还没定义好的字段
  2. computedthisD(data 的返回类型)
  3. methodsthisD & M & 各 computed 的返回类型

框架的「魔法」= 类型层面的映射 + ThisType。type-challenges 的 00006 SimpleVue / 00213 VueBasicProps / 01290 Pinia 三道 hard 题,本质就是让你亲手写出这种签名。

与第 4 部分的关系

这三道 hard 题还用到条件类型与 ReturnType(第 7 章)、映射类型(第 6 章)。如果你感觉吃力,先去第 13-14 章把递归和元组练扎实再回来。


本章小结

  • 类:字段 / 构造器(含参数属性)/ 修饰符 / 静态成员;private/readonly 是编译期检查
  • 抽象类定骨架,implements 定契约
  • 装饰器分 legacy 与 TC39 两套,重在「它在框架里怎么用」,别死磕语法
  • this: T 显式标注回调的 this;ThisType<T> 驱动选项式 API 推导
  • Vue / Pinia 的「魔法类型」= 映射 + 条件类型 + ThisType

本章练习

挑战题(难度较高,建议学完第 4 部分再回做)

题号题目难度考察点
00006简单的 Vue 类型hardThisType + 映射 computed 返回
00213Vue Basic Propshardprops 配置 → 类型 + ThisType
01290Piniahardgetter/action 的 this 分工
00017柯里化 1hard函数式 + infer 递归

这 4 道是全书最接近「真实框架类型推导」的题。做完 01290 Pinia,你对「一个 store 的类型怎么被推出来」会豁然开朗。

代码练习

  1. 访问修饰符:写一个 BankAccount 类,用 private 保护余额、readonly 保护账户名、protected 给子类用,验证外部访问会报错。

  2. 抽象类 + implements:定义一个 Animal 抽象类(抽象方法 speak()),让 DogCat 实现;再定义一个 Flyable 接口,让 Bird 同时 extends Animalimplements Flyable

  3. ThisType 选项式:复刻 10.4.2 的 create,给它加一个 computed 区块(用映射让 methods 里能访问 computed 的返回值类型)。

  4. this 标注:写一个 this: void 的回调函数,并写一个 this: SomeContext 的方法,验证在错误调用方式下会报错。

  5. 理解 SimpleVue 签名:不看答案,试着为 SimpleVue 写一份类型签名(提示:三个泛型 D/C/M + 三处 ThisType + computed 返回映射)。能写出大框架即可,不必一次全对。