类与装饰器

2026-09-09 00:00    #typescript  

类在 TypeScript 里有两个身份:运行时它是一个构造函数,编译期它是一个类型。这个双重身份是理解类成员修饰符、implements、装饰器的前提。装饰器则是"在类定义时修改类行为"的元编程能力,它有两条互不兼容的路线,选择哪条决定了你的 tsconfig 和整个技术栈。

类成员与修饰符

 1// 类成员与修饰符
 2
 3class Account {
 4  // 实例字段:每个实例一份
 5  balance = 0;
 6
 7  // 只读字段:只能在声明处或构造函数里赋值
 8  readonly id: string;
 9
10  // 静态字段:挂在类上,不是实例上
11  static totalAccounts = 0;
12
13  // 私有字段(TS 修饰符):只在编译期检查
14  private secret = "s";
15
16  // 私有字段(ES 真私有):运行时也访问不到
17  #realSecret = "r";
18
19  // 受保护:子类可以访问
20  protected owner: string;
21
22  // 可选字段
23  nickname?: string;
24
25  constructor(id: string, owner: string) {
26    this.id = id;
27    this.owner = owner;
28    Account.totalAccounts++;
29  }
30
31  // getter / setter
32  get label(): string {
33    return `${this.id}(${this.owner})`;
34  }
35
36  set label(v: string) {
37    this.nickname = v;
38  }
39
40  // 静态方法
41  static reset(): void {
42    Account.totalAccounts = 0;
43  }
44
45  // 读取私有字段的方法
46  reveal(): string {
47    return `${this.secret}/${this.#realSecret}`;
48  }
49}
50
51// 参数属性:构造函数参数前加修饰符,自动成为字段
52class Point {
53  constructor(
54    public x: number,
55    private y: number,
56    readonly z: number = 0,
57  ) {}
58
59  // y 是 private,只能在类内部读
60  sum(): number {
61    return this.x + this.y + this.z;
62  }
63}
64
65// 用 # 真私有:运行时也拿不到
66class Vault {
67  #value = 1;
68  get(): number { return this.#value; }
69}
70
71const a = new Account("a1", "rainboy");
72a.label = "alias";
73a.balance = 100;
74
75const p = new Point(1, 2);
76
77console.log(a.label, a.balance, a.nickname, a.reveal(), Account.totalAccounts);
78console.log(p.x, p.sum(), new Vault().get());
79
80// 验证 # 真私有:类外访问是语法错误(不是类型错误)
81// 下面这行连解析都过不去,所以不能写在源码里:
82//   const v = new Vault();
83//   console.log(v.#value);   // SyntaxError: Private field '#value' must be declared in an enclosing class
84//
85// 对比:TS 的 private 只是编译期检查,运行时能绕过
86const acc = new Account("a2", "x");
87console.log((acc as unknown as { secret: string }).secret);   // "s",运行时确实能读到
1a1(rainboy) 100 alias s/r 1
21 3 1
3s

字段的种类

种类写法归属
实例字段balance = 0每个实例一份
只读字段readonly id: string只能在声明处或构造函数里赋值
静态字段static total = 0挂在类上,不在实例上
可选字段nickname?: string类型是 string | undefined

两种"私有"

这是最容易混淆的地方:

写法检查时机能否运行时绕过
private secret只在编译期能,运行时属性真实存在
#realSecret编译期 + 运行时不能,是真正的私有槽

实测证明 private 只是编译期检查:

1class Account { private secret = "s"; }
2const a = new Account();
3console.log((a as any).secret);   // "s" —— 运行时确实能读到

而 # 私有字段连解析都过不去:

1const v = new Vault();
2console.log(v.#value);
3// SyntaxError: Private field '#value' must be declared in an enclosing class

注意这是语法错误而不是类型错误——它连编译都到不了。所以:

参数属性

构造函数参数前加修饰符,会自动声明字段并赋值:

1class Point {
2  constructor(
3    public x: number,
4    private y: number,
5    readonly z: number = 0,
6  ) {}
7}

这是不可擦除语法——它生成真正的赋值代码(见 类型擦除)。编译产物里 this.x = x 是显式存在的。

getter / setter

1get label(): string { return `${this.id}(${this.owner})`; }
2set label(v: string) { this.nickname = v; }

一个细节:如果只有 get 没有 set,属性自动变成 readonly。这在 TypeScript 4.3 之后是默认行为。

继承、抽象类与 implements

 1// 继承、抽象类、implements
 2
 3abstract class Shape {
 4  // 抽象方法:子类必须实现
 5  abstract area(): number;
 6  abstract readonly kind: string;
 7
 8  // 普通方法:子类继承
 9  describe(): string {
10    return `${this.kind} area=${this.area().toFixed(2)}`;
11  }
12
13  // 模板方法:定义流程,子类填细节
14  report(): string {
15    return `[${this.describe()}]`;
16  }
17}
18
19class Circle extends Shape {
20  readonly kind = "circle";
21  constructor(private radius: number) { super(); }
22  area(): number { return Math.PI * this.radius ** 2; }
23}
24
25class Square extends Shape {
26  readonly kind = "square";
27  constructor(private side: number) { super(); }
28  area(): number { return this.side ** 2; }
29}
30
31// 不能实例化抽象类
32// @ts-expect-error TS2511: 不能创建抽象类的实例
33new Shape();
34
35// implements:只检查实例侧的形状,不继承实现
36interface Serializable {
37  serialize(): string;
38}
39
40interface Comparable<T> {
41  compareTo(other: T): number;
42}
43
44class Version implements Serializable, Comparable<Version> {
45  constructor(private major: number, private minor: number) {}
46
47  serialize(): string {
48    return `${this.major}.${this.minor}`;
49  }
50
51  compareTo(other: Version): number {
52    return this.major - other.major || this.minor - other.minor;
53  }
54}
55
56// 类可以同时 extends 和 implements
57class NamedCircle extends Circle implements Serializable {
58  constructor(radius: number, private name: string) { super(radius); }
59  serialize(): string { return `${this.name}:${this.area().toFixed(2)}`; }
60}
61
62// implements 只检查形状:私有字段会导致"名义化"检查
63class HasPrivate {
64  private secret = "s";
65}
66// 下面这个类虽然结构相同,但因为 private 不同源,不能互相赋值
67class OtherPrivate {
68  private secret = "s";
69}
70// @ts-expect-error TS2322: 私有字段来源不同
71const hp: HasPrivate = new OtherPrivate();
72
73// override 关键字:显式标记覆写
74class Base {
75  greet(): string { return "base"; }
76}
77class Derived extends Base {
78  override greet(): string { return "derived"; }
79}
80
81const shapes: Shape[] = [new Circle(1), new Square(2)];
82console.log(shapes.map((s) => s.report()));
83console.log(new Version(1, 2).serialize(), new NamedCircle(1, "c").serialize());
84console.log(new Derived().greet());
1[ '[circle area=3.14]', '[square area=4.00]' ]
21.2 c:3.14
3derived

抽象类

1abstract class Shape {
2  abstract area(): number;       // 子类必须实现
3  describe(): string { ... }      // 子类继承
4}

抽象类有两个作用:

  1. 不能实例化:new Shape() 报 error TS2511: Cannot create an instance of an abstract class.
  2. 定义模板方法:父类定义流程(report() 调用 describe() 调用 area()),子类只填 area()

implements 检查什么

implements 只检查实例侧的形状,不继承任何实现:

1interface Serializable { serialize(): string }
2
3class Version implements Serializable, Comparable<Version> {
4  serialize(): string { ... }     // 必须自己实现
5}

关键区别:

extendsimplements
继承实现✓✗
继承类型✓✗(只检查)
数量只能一个可以多个
目标类类或接口

private 破坏结构类型

TypeScript 通常是结构类型(看形状),但 private 字段是个例外:

1class HasPrivate { private secret = "s" }
2class OtherPrivate { private secret = "s" }
3
4// @ts-expect-error TS2322: 私有字段来源不同
5const hp: HasPrivate = new OtherPrivate();

两个类结构完全一样,但因为 private 来自不同的声明,TypeScript 认为它们不兼容。这是名义化(nominal)检查的少数场景。

override 关键字

显式标记覆写父类方法:

1class Derived extends Base {
2  override greet(): string { return "derived"; }
3}

override 本身是可选的,但配合 noImplicitOverride 开启后强制要求:

1class Derived extends Base {
2  greet(): string { return "d"; }
3  // error TS4114: This member must have an 'override' modifier because it overrides
4  // a member in the base class 'Base'.
5}

它能防止"父类方法改名后子类不再覆写它"的隐蔽 bug。

this 类型与泛型类

 1// 类的 this 类型、泛型类、类与接口的关系
 2
 3// this 类型:返回 this 实现链式调用
 4class QueryBuilder {
 5  private parts: string[] = [];
 6
 7  select(cols: string): this {
 8    this.parts.push(`select ${cols}`);
 9    return this;
10  }
11
12  from(table: string): this {
13    this.parts.push(`from ${table}`);
14    return this;
15  }
16
17  build(): string {
18    return this.parts.join(" ");
19  }
20}
21
22class FilterBuilder extends QueryBuilder {
23  where(cond: string): this {
24    // 只能通过 public 方法访问 parts,这里用 build 演示
25    void cond;
26    return this;
27  }
28}
29
30const q = new QueryBuilder().select("*").from("users").build();
31// 继承链上 this 依然正确:FilterBuilder 的方法可链式调用父类方法
32const f = new FilterBuilder().select("*").where("id=1").build();
33
34// 泛型类 + 泛型约束
35interface Entity {
36  id: string;
37}
38
39class Repository<T extends Entity> {
40  private items = new Map<string, T>();
41
42  save(item: T): void {
43    this.items.set(item.id, item);
44  }
45
46  find(id: string): T | undefined {
47    return this.items.get(id);
48  }
49
50  all(): T[] {
51    return [...this.items.values()];
52  }
53}
54
55interface User extends Entity {
56  name: string;
57}
58
59const repo = new Repository<User>();
60repo.save({ id: "1", name: "rainboy" });
61
62// 泛型静态方法(静态方法不能用类的类型参数)
63class Box<T> {
64  constructor(private value: T) {}
65  get(): T { return this.value; }
66
67  // 静态方法必须自己声明类型参数
68  static of<U>(v: U): Box<U> {
69    return new Box(v);
70  }
71
72  // 下面这样写会报错:静态成员不能引用类类型参数
73  // static bad(v: T): Box<T> { return new Box(v); }
74}
75
76const box = Box.of(1);
77
78// 类实现接口时的可见性:implements 只要求 public 部分
79interface Readable {
80  read(): string;
81}
82class File implements Readable {
83  private content = "data";
84  read(): string { return this.content; }   // 这个方法必须是 public
85}
86
87console.log(q, f);
88console.log(repo.find("1")?.name, repo.all().length, box.get(), new File().read());
1select * from users select *
2rainboy 1 1 data

this 类型实现链式调用

1class QueryBuilder {
2  select(cols: string): this { ...; return this; }
3  from(table: string): this { ...; return this; }
4}

返回类型写 this(而不是 QueryBuilder)的好处:子类继承后链式调用不丢类型。

1class FilterBuilder extends QueryBuilder {
2  where(cond: string): this { ...; return this; }
3}
4
5new FilterBuilder().select("*").where("id=1").build();
6// 如果 select 返回 QueryBuilder,这里 where 就调用不到了

泛型类

1class Repository<T extends Entity> {
2  save(item: T): void { }
3  find(id: string): T | undefined { }
4}

泛型参数绑定在实例上,创建时确定。

静态方法不能引用类的类型参数

1class Box<T> {
2  static of<U>(v: U): Box<U> { return new Box(v); }   // 自己声明 U
3
4  static bad(v: T): Box<T> { }
5  // error TS2302: Static members cannot reference class type parameters.
6}

原因:静态成员属于类本身,而 T 属于实例。类存在时还没有实例,T 无从确定。静态方法需要泛型就自己声明一个。

装饰器:两条路线

装饰器是"在类定义时修改类行为"的语法。TypeScript 支持两套语义不同、不能混用的装饰器:

legacy标准(TC39 stage-3)
配置experimentalDecorators: true无需配置
来源TypeScript 早期实验实现ECMAScript 标准提案
元数据emitDecoratorMetadata 支持无
签名(target, key, descriptor)(value, context)
字段装饰器不能改初始值返回函数可改初始值
谁在用nestjs、TypeORM、Angular新项目、库作者
TS 版本一直支持TS 5.0+
两者不能共存

同一个类不能既有 legacy 装饰器又有标准装饰器。experimentalDecorators 是全局开关,开了之后所有装饰器都按 legacy 语义解释。所以本仓库给两条路线各配了一份 tsconfig.json:

  • src/decorators-legacy/tsconfig.json → experimentalDecorators: true
  • src/decorators-standard/tsconfig.json → 无此选项

Legacy 装饰器

 1// legacy 装饰器:类装饰器
 2
 3// 类装饰器接收构造函数,可以返回一个新类替换它
 4function sealed<T extends new (...args: any[]) => object>(ctor: T): T {
 5  Object.seal(ctor);
 6  Object.seal(ctor.prototype);
 7  return ctor;
 8}
 9
10// 不替换,只做副作用(常见于注册)
11function register(ctor: new (...args: any[]) => object): void {
12  console.log(`register: ${ctor.name}`);
13}
14
15@sealed
16@register
17class BugReport {
18  type = "report";
19  title: string;
20
21  constructor(t: string) {
22    this.title = t;
23  }
24}
25
26// 返回新类:替换原类
27function withTimestamp<T extends new (...args: any[]) => object>(ctor: T) {
28  return class extends ctor {
29    createdAt = new Date("2020-01-01");
30  };
31}
32
33@withTimestamp
34class Post {
35  title = "p";
36}
37
38const p = new Post();
39// 装饰器顺序:靠近类的先执行(register 先,然后 sealed)
40// 但工厂函数的求值顺序相反(从上到下)
41
42console.log(new BugReport("bug").title, p.title, (p as Post & { createdAt: Date }).createdAt.toISOString());
1register: BugReport
2bug p 2020-01-01T00:00:00.000Z

五种 legacy 装饰器

位置签名
类(ctor) => ctor | void
方法(target, key, descriptor) => descriptor | void
属性(target, key) => void
访问器(target, key, descriptor) => descriptor | void
参数(target, key, parameterIndex) => void
 1// legacy 方法/属性/参数装饰器
 2
 3// 方法装饰器:三个参数 (target, propertyKey, descriptor)
 4function log(
 5  target: object,
 6  propertyKey: string,
 7  descriptor: PropertyDescriptor,
 8): void {
 9  const original = descriptor.value as (...args: unknown[]) => unknown;
10  descriptor.value = function (this: unknown, ...args: unknown[]) {
11    console.log(`call ${propertyKey}(${args.join(",")})`);
12    return original.apply(this, args);
13  };
14}
15
16// 属性装饰器:只有 (target, propertyKey),没有 descriptor
17function readonly(target: object, propertyKey: string): void {
18  Object.defineProperty(target, propertyKey, { writable: false });
19}
20
21// 参数装饰器:(target, propertyKey, parameterIndex)
22function inject(
23  target: object,
24  propertyKey: string | undefined,
25  parameterIndex: number,
26): void {
27  console.log(`inject into ${String(propertyKey)}[${parameterIndex}]`);
28}
29
30class Calculator {
31  @readonly
32  version = "1.0";
33
34  @log
35  add(a: number, b: number): number {
36    return a + b;
37  }
38
39  @log
40  greet(@inject name: string): string {
41    return `hi ${name}`;
42  }
43}
44
45const c = new Calculator();
46console.log(c.add(1, 2));
47console.log(c.greet("rainboy"));
48console.log(c.version);
49
50// 验证 readonly:属性装饰器用 defineProperty 让字段不可写
51// 注意:严格模式下赋值会抛错,非严格模式静默失败
52try {
53  (c as unknown as { version: string }).version = "2.0";
54  console.log("赋值后:", c.version);
55} catch (e) {
56  console.log("赋值被拦截:", (e as Error).message);
57}
1call add(1,2)
23
3call greet(rainboy)
4hi rainboy
51.0
6赋值后: 1.0

类装饰器可以替换类

 1function withTimestamp<T extends new (...args: any[]) => object>(ctor: T) {
 2  return class extends ctor {
 3    createdAt = new Date("2020-01-01");
 4  };
 5}
 6
 7@withTimestamp
 8class Post { title = "p"; }
 9
10const p = new Post();
11p.createdAt;
12// error TS2339: Property 'createdAt' does not exist on type 'Post'.

类型不会跟着更新——这是 legacy 类装饰器的已知限制。要用到新字段必须断言:

1(p as Post & { createdAt: Date }).createdAt;

应用顺序

多个装饰器从下往上应用(靠近类/成员的先执行):

1@prefix("Mr. ")      // 后执行
2@upperCase           // 先执行
3get name() { return this._name; }
4// 结果:Mr. RAINBOY

但装饰器工厂的求值是从上往下:

1@factoryA()   // 先求值(拿到装饰器函数)
2@factoryB()   // 后求值
3class C {}

emitDecoratorMetadata

开启后,TypeScript 会把类型信息写进装饰器元数据:

 1// emitDecoratorMetadata:装饰器能拿到类型元信息
 2//
 3// 重要:tsx / esbuild 不实现 emitDecoratorMetadata,所以这个文件必须用 tsc 编译再运行:
 4//   npx tsc -p content/typescript/src/decorators-legacy --outDir content/typescript/src/decorators-legacy/.emit --noEmit false
 5//   node content/typescript/src/decorators-legacy/.emit/metadata.js
 6// 实测输出:find: params=[Number,String] return=Boolean
 7// 用 tsx 跑同一文件:params=[] return=undefined(元数据没生成)
 8//
 9// 依赖 reflect-metadata(提供 Reflect.getMetadata / Reflect.metadata)
10
11import "reflect-metadata";
12
13function describe(target: object, propertyKey: string): void {
14  const paramTypes = Reflect.getMetadata("design:paramtypes", target, propertyKey);
15  const returnType = Reflect.getMetadata("design:returntype", target, propertyKey);
16  const names = paramTypes?.map((t: { name: string }) => t.name) ?? [];
17  console.log(`${propertyKey}: params=[${names}] return=${returnType?.name}`);
18}
19
20class Service {
21  @describe
22  find(id: number, name: string): boolean {
23    void id;
24    void name;
25    return true;
26  }
27}
28
29console.log(new Service().find(1, "a"));

三个可读的 key:

1Reflect.getMetadata("design:type", target, key)         // 属性类型
2Reflect.getMetadata("design:paramtypes", target, key)   // 参数类型数组
3Reflect.getMetadata("design:returntype", target, key)   // 返回类型
tsx / esbuild 不实现 emitDecoratorMetadata

这是实践中很容易踩的坑。同一个文件:

1# 用 tsc 编译后运行 —— 元数据存在
2$ npx tsc -p content/typescript/src/decorators-legacy --outDir .../.emit --noEmit false
3$ node .../.emit/metadata.js
4find: params=[Number,String] return=Boolean
5
6# 用 tsx 直接运行 —— 元数据丢失
7$ npx tsx content/typescript/src/decorators-legacy/metadata.ts
8find: params=[] return=undefined

reflect-metadata 是必需的运行时依赖,它提供 Reflect.getMetadata。nestjs 的依赖注入正是靠 design:paramtypes 拿到构造函数参数类型来决定注入什么——所以 nestjs 项目必须用 tsc(或配置了元数据的 swc)编译,不能用 esbuild/tsx 直接跑。

标准装饰器(TS 5.0+)

 1// 标准装饰器(TC39 stage-3,TS 5.0+)
 2// 不需要 experimentalDecorators
 3
 4// 方法装饰器:(value, context)
 5function logged<T extends (...args: any[]) => any>(
 6  fn: T,
 7  ctx: ClassMethodDecoratorContext,
 8): T {
 9  return function (this: unknown, ...args: unknown[]) {
10    console.log(`call ${String(ctx.name)}`);
11    return fn.apply(this, args);
12  } as T;
13}
14
15// 装饰器工厂
16function withRetry(times: number) {
17  return function <T extends (...args: any[]) => any>(
18    fn: T,
19    ctx: ClassMethodDecoratorContext,
20  ): T {
21    return function (this: unknown, ...args: unknown[]) {
22      let last: unknown;
23      for (let i = 0; i < times; i++) {
24        try {
25          return fn.apply(this, args);
26        } catch (e) {
27          last = e;
28        }
29      }
30      throw last;
31    } as T;
32  };
33}
34
35class Calc {
36  @logged
37  add(a: number, b: number): number {
38    return a + b;
39  }
40
41  @withRetry(3)
42  flaky(failTimes: number): string {
43    // 用一个计数器模拟不稳定
44    if (this.attempts++ < failTimes) throw new Error("fail");
45    return "ok";
46  }
47
48  attempts = 0;
49}
50
51const c = new Calc();
52console.log(c.add(1, 2));
53console.log(c.flaky(2));
54console.log("context 提供的字段:", Calc.name);
1call add
23
3ok
4context 提供的字段: Calc

标准装饰器的签名是 (value, context),context 里有:

字段含义
name成员名(可能是 symbol)
kind"class" / "method" / "field" / "getter" / "setter" / "accessor"
static是否静态成员
private是否私有
metadata装饰器间传递数据的对象
access{ get, set, has },访问成员的能力
addInitializer注册初始化回调
 1// 标准装饰器的 context 对象与各类装饰器
 2
 3// 1. 类装饰器:(value, context: ClassDecoratorContext)
 4function tagged<T extends new (...args: any[]) => object>(
 5  value: T,
 6  ctx: ClassDecoratorContext,
 7): T {
 8  console.log(`class ${String(ctx.name)}, kind=${ctx.kind}`);
 9  ctx.addInitializer(function (this: object) {
10    (this as { tagged?: boolean }).tagged = true;
11  });
12  return value;
13}
14
15// 2. 字段装饰器:返回初始化函数
16function double(_value: undefined, ctx: ClassFieldDecoratorContext) {
17  return function (this: unknown, initial: number) {
18    console.log(`field ${String(ctx.name)} 初始化`);
19    return initial * 2;
20  };
21}
22
23// 3. 访问器装饰器
24function loggedAccessor(
25  value: { get: () => unknown; set: (v: unknown) => void },
26  ctx: ClassAccessorDecoratorContext,
27) {
28  console.log(`accessor ${String(ctx.name)}`);
29  return {
30    get() { return value.get.call(this); },
31    set(v: unknown) { value.set.call(this, v); },
32  };
33}
34
35// 4. getter 装饰器:返回类型必须匹配原 getter
36function upper(
37  getter: (this: unknown) => string,
38  ctx: ClassGetterDecoratorContext,
39): (this: unknown) => string {
40  void ctx;
41  return function (this: unknown) {
42    return getter.call(this).toUpperCase();
43  };
44}
45
46// 5. 静态成员装饰器
47function staticLog<T extends (...args: any[]) => any>(
48  fn: T,
49  ctx: ClassMethodDecoratorContext,
50): T {
51  console.log(`static method ${String(ctx.name)}, static=${ctx.static}`);
52  return fn;
53}
54
55@tagged
56class Config {
57  @double
58  level = 1;
59
60  accessor name = "rainboy";
61
62  @upper
63  get title(): string {
64    return "hello";
65  }
66
67  @staticLog
68  static create(): Config {
69    return new Config();
70  }
71}
72
73const c = Config.create();
74console.log(c.level, c.name, c.title, (c as { tagged?: boolean }).tagged);
75
76// context 对象的字段(实测)
77console.log("context 字段: name, kind, static, private, metadata, access");
1static method create, static=true
2class Config, kind=class
3field level 初始化
42 rainboy HELLO undefined
5context 字段: name, kind, static, private, metadata, access

各类标准装饰器

 1// 类:接收构造函数
 2function tagged(value: T, ctx: ClassDecoratorContext) { return value; }
 3
 4// 方法:接收函数,返回替换函数
 5function logged(fn: T, ctx: ClassMethodDecoratorContext) { return wrapper; }
 6
 7// 字段:返回初始化函数,可以改初始值
 8function double(_v: undefined, ctx: ClassFieldDecoratorContext) {
 9  return function (initial: number) { return initial * 2; };
10}
11
12// getter:返回类型必须匹配原 getter
13function upper(getter: (this: unknown) => string, ctx: ClassGetterDecoratorContext) {
14  return function (this: unknown) { return getter.call(this).toUpperCase(); };
15}

注意 context 类型的准确名称:是 ClassAccessorDecoratorContext(不是 ClassAutoAccessorDecoratorContext),这个笔误很容易犯。

addInitializer 的执行时机

这是标准装饰器最需要实测的一点:

装饰器位置addInitializer 执行时机this 是
类装饰器类定义时执行一次类本身
字段/方法装饰器每个实例构造时实例

实测验证:

1装饰器执行, this 是 class: App
2class 级 initializer 执行, this = 类本身
3--- 创建实例 1 ---
4field 级 initializer 执行, this 是实例: true
5--- 创建实例 2 ---
6field 级 initializer 执行, this 是实例: true

所以给实例加属性必须用字段装饰器的 addInitializer:

 1// 错误:类装饰器里的 this 是类,不是实例
 2function withInit(value: T, ctx: ClassDecoratorContext) {
 3  ctx.addInitializer(function () {
 4    (this as any).ready = true;    // 挂到类上了,实例拿不到
 5  });
 6}
 7
 8// 正确:字段装饰器的 this 是实例
 9function perInstance(_v: undefined, ctx: ClassFieldDecoratorContext) {
10  ctx.addInitializer(function () {
11    (this as any).ready = true;    // 每个实例都有
12  });
13}

两条路线的差异对照

 1// legacy 与标准装饰器的语义差异(同一场景两种写法)
 2
 3// ============ 差异 1:装饰器函数签名 ============
 4// legacy: (target, propertyKey, descriptor)
 5// 标准:   (value, context)
 6// context 里有 name / kind / static / private / metadata / addInitializer
 7
 8// ============ 差异 2:this 的处理 ============
 9// 标准装饰器里 this 是 unknown,必须显式声明
10
11// ============ 差异 3:字段装饰器返回值含义 ============
12// legacy 字段装饰器无返回值(不能改初始值)
13// 标准字段装饰器返回一个初始化函数,可以改初始值
14function legacyStyle(target: object, key: string): void {
15  void target; void key;
16}
17function standardStyle(_v: undefined, ctx: ClassFieldDecoratorContext) {
18  return function (initial: string) {
19    return `${initial}!`;   // 可以改初始值
20  };
21}
22
23// ============ 差异 4:addInitializer 的执行时机(关键差异)============
24// 实测结论:
25//   - 类装饰器里的 addInitializer:在【类定义时】执行一次,this 是类本身
26//   - 字段/方法装饰器里的 addInitializer:在【每个实例构造时】执行,this 是实例
27function withInit<T extends new (...args: any[]) => object>(
28  value: T,
29  ctx: ClassDecoratorContext,
30): T {
31  ctx.addInitializer(function (this: unknown) {
32    // 注意:这里 this 是类,不是实例!所以下面这样写没用
33    console.log(`[class initializer] ${String(ctx.name)}, this === 类: ${this === value}`);
34  });
35  return value;
36}
37
38// 要给每个实例加属性,得用字段装饰器的 addInitializer
39function perInstance(_v: undefined, ctx: ClassFieldDecoratorContext) {
40  ctx.addInitializer(function (this: unknown) {
41    (this as { ready?: boolean }).ready = true;   // 这里 this 才是实例
42  });
43}
44
45// legacy 要这样写(对比):
46function legacyWithInit<T extends new (...args: any[]) => object>(ctor: T): T {
47  const orig = ctor;
48  return class extends orig {
49    constructor(...args: any[]) {
50      super(...args);
51      (this as { ready?: boolean }).ready = true;
52    }
53  };
54}
55
56// ============ 差异 5:元数据 ============
57// legacy + emitDecoratorMetadata 可拿到类型(依赖 reflect-metadata)
58// 标准装饰器没有元数据机制,要自己实现
59
60@withInit
61class App {
62  @standardStyle
63  name = "app";
64
65  @perInstance
66  ready = false;
67}
68
69const a = new App();
70console.log(a.name, (a as { ready?: boolean }).ready);
71
72// ============ 差异 6:tsconfig ============
73// legacy: "experimentalDecorators": true(可加 emitDecoratorMetadata)
74// 标准:   什么都不用配;两者不能同时用于同一个类
75console.log("legacyStyle/legacyWithInit 仅作对比,未使用:", typeof legacyStyle, typeof legacyWithInit);
1[class initializer] App, this === 类: true
2app! true
3legacyStyle/legacyWithInit 仅作对比,未使用: function function
维度legacy标准
装饰器签名(target, key, descriptor)(value, context)
改字段初始值不支持返回函数即可
给实例加属性返回子类 hack字段装饰器 addInitializer
类型元数据emitDecoratorMetadata无,需自己实现
this 类型隐式 anyunknown,必须显式声明
工具链兼容只有 tsc 完整支持esbuild/swc 均支持

该选哪条

选 legacy 的情况:

选标准的情况:

混合工具链的现实

如果你想用 esbuild/swc 的高速构建,又想用装饰器,只能选标准装饰器。这是 nestjs 生态目前的结构性约束——它需要元数据,所以必须走 tsc 或专门配置的 swc。详见 运行 TypeScript。

常见错误速查

错误码消息触发场景
TS2511Cannot create an instance of an abstract class.new 抽象类
TS2322Type 'OtherPrivate' is not assignable to type 'HasPrivate'.private 字段来源不同
TS2302Static members cannot reference class type parameters.静态成员引用类类型参数
TS2339Property 'createdAt' does not exist on type 'Post'.类装饰器替换类后类型未更新
TS2540Cannot assign to 'x' because it is a read-only property.只有 getter 没有 setter
TS4114This member must have an 'override' modifier...开启 noImplicitOverride 后漏写 override
TS2552Cannot find name 'ClassAutoAccessorDecoratorContext'.context 类型名写错
TS1270Decorator function return type ... is not assignable to type ...标准装饰器返回类型不匹配

小结

主题核心结论
private vs #前者只编译期,后者运行时真私有
参数属性不可擦除,生成赋值代码
抽象类不能实例化,用于模板方法模式
implements只检查形状,不继承实现
this 类型链式调用返回 this,子类不丢类型
静态成员不能引用类的类型参数
legacy 装饰器5 种签名,支持元数据,nestjs 依赖它
标准装饰器(value, context),addInitializer 时机随位置不同
两者关系全局互斥,experimentalDecorators 一开全变 legacy

下一篇:工程化。