Typescript 5 装饰器
Typescript 5 装饰器

Typescript 5 装饰器

Tags
前端
技术
Published
June 20, 2024
Author
装饰器现在其实已经广泛应用于前端领域,以及一些框架和开发工具中。例如:在 React 开发中,Redux 使用装饰器来连接组件和状态;在 NestJS 框架中,装饰器被用于实现依赖注入和中间件。这些都是利用装饰器实现的,使我们的代码更加简洁,逻辑更加清晰。
随着 TypeScript 5.0 版本正式支持 Stage3 的 Decorators(装饰器) - 提案,装饰器这个特性可以被我们更加广泛的应用。且进入 Stage3 阶段的提案往往也代表着不会有大的变更,本文也将基于这个版本的装饰器使用进行入门的探索。

装饰器的历史

装饰器的概念源于 Python 和其他使用类似结构的编程语言,主要用于修改或扩展类、方法和属性的行为。
最初的 JavaScript 装饰器提案于 2014 年提出,自那时以来,已经开发出多个版本的提案,目前的版本正在 ECMAScript 标准化过程的第 3 阶段。

过往:实验性装饰器

实际上,TypeScript 在 5.0 版本之前就已经支持了实验性的装饰器。但是,由于它是基于较早的提案来实现的,因此与最新的提案存在一些差异:
  1. 需要在 tsconfig 中开启 experimentalDecorators
  1. decorator 方法与 Stage3 中的参数有差异
  1. 元数据需要通过 reflect-metadata 来获取,如类型等
本文省略这部分内容,感兴趣的可以查看这个文档

现在:Stage3 装饰器

现在基于 Stage3 提案的 Decorators 在默认配置下就可以使用了,如果 tsconfig 中指定了 experimentalDecorators 需要移除,否则还是会 fallback 到之前的实验性装饰器上。
 
新的装饰器 Decorator 的方法接受两个参数,类型定义如下:
type Decorator<T> = ( value: T, // 对应的字段值 context: DecoratorContext, ) => void | T;
以上的装饰器类型又分为以下几种:
Class、Method、Getter、Setter、Field、Accessor
type ClassMemberDecoratorContext = | ClassMethodDecoratorContext | ClassGetterDecoratorContext | ClassSetterDecoratorContext | ClassFieldDecoratorContext | ClassAccessorDecoratorContext; type DecoratorContext = | ClassDecoratorContext | ClassMemberDecoratorContext;

Class

类型定义

interface ClassDecoratorContext< Class extends abstract new (...args: any) => any = abstract new (...args: any) => any, > { /** The kind of element that was decorated. */ readonly kind: "class"; /** The name of the decorated class. */ readonly name: string | undefined; /** * Adds a callback to be invoked after the class definition has been finalized. * * @example * ```ts * function customElement(name: string): ClassDecoratorFunction { * return (target, context) => { * context.addInitializer(function () { * customElements.define(name, this); * }); * } * } * * @customElement("my-element") * class MyElement {} * ``` */ addInitializer(initializer: (this: Class) => void): void; readonly metadata: DecoratorMetadata; }

实例

  • 收集初始化的实例
class InstanceCollector { instances = new Set(); install = <Class extends Constructor>( value: Class, context: ClassDecoratorContext, ) => { const _this = this; return class extends value { constructor(...args: any[]) { console.log( "class constructor", context.name, context.metadata, context.kind, ); super(...args); _this.instances.add(this); } }; }; } const collector = new InstanceCollector(); @collector.install class MyClass { name: string; constructor(name: string) { this.name = name; } } const inst1 = new MyClass("1"); const inst2 = new MyClass("2"); const inst3 = new MyClass("3"); console.log("instances: ", collector.instances); /** Console instances: Set(3) { MyClass { name: '1' }, MyClass { name: '2' }, MyClass { name: '3' } } **/

Method

类型定义

/** * Context provided to a class method decorator. * @template This The type on which the class element will be defined. For a static class element, this will be * the type of the constructor. For a non-static class element, this will be the type of the instance. * @template Value The type of the decorated class method. */ interface ClassMethodDecoratorContext< This = unknown, Value extends (this: This, ...args: any) => any = (this: This, ...args: any) => any, > { /** The kind of class element that was decorated. */ readonly kind: "method"; /** The name of the decorated class element. */ readonly name: string | symbol; /** A value indicating whether the class element is a static (`true`) or instance (`false`) element. */ readonly static: boolean; /** A value indicating whether the class element has a private name. */ readonly private: boolean; /** An object that can be used to access the current value of the class element at runtime. */ readonly access: { /** * Determines whether an object has a property with the same name as the decorated element. */ has(object: This): boolean; /** * Gets the current value of the method from the provided object. * * @example * let fn = context.access.get(instance); */ get(object: This): Value; }; /** * Adds a callback to be invoked either before static initializers are run (when * decorating a `static` element), or before instance initializers are run (when * decorating a non-`static` element). * * @example * ```ts * const bound: ClassMethodDecoratorFunction = (value, context) { * if (context.private) throw new TypeError("Not supported on private methods."); * context.addInitializer(function () { * this[context.name] = this[context.name].bind(this); * }); * } * * class C { * message = "Hello"; * * @bound * m() { * console.log(this.message); * } * } * ``` */ addInitializer(initializer: (this: This) => void): void; readonly metadata: DecoratorMetadata; }

实际应用

个人认为最实用的一个装饰器,目前想到一些比较实用的用法如下:
  • 对函数的入参做合法性校验
  • 对于函数的调用打 log
  • try catch 做容错处理

示例

下面这个示例是对函数调用打 log 和 try catch 处理
function log(originalMethod: Function, context: ClassMethodDecoratorContext) { // console.log("log", context); const methodName = String(context.name); function replacementMethod(this: any, ...args: any[]) { const className = String(this.constructor.name); console.log(`LOG: Entering method '${className}.${methodName}'.`); const result = originalMethod.call(this, ...args); console.log(`LOG: Exiting method '${className}.${methodName}'.`); return result; } return replacementMethod; } function catchErr( originalMethod: Function, context: ClassMethodDecoratorContext, ) { const methodName = String(context.name); return function (this: any, ...args: any[]) { const className = String(this.constructor.name); try { const result = originalMethod.call(this, ...args); return result; } catch (e) { console.error(`Catch A Error in method '${className}.${methodName}'.`, e); return null; } }; } class BankService { @log callAPI() { console.log("call api"); } // 组合装饰器 @log @catchErr callAPIWithError() { throw new Error("error"); } } const service = new BankService(); service.callAPI(); service.callAPIWithError(); console.log("----exit-----"); /** Console LOG: Entering method 'BankService.callAPI'. call api LOG: Exiting method 'BankService.callAPI'. LOG: Entering method 'BankService.callAPIWithError'. Catch A Error in method 'BankService.callAPIWithError'. /Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/method.ts:41 throw new Error("error"); ^ Error: error at _BankService.callAPIWithError (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/method.ts:41:11) at _BankService.<anonymous> (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/method.ts:22:37) at _BankService.replacementMethod (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/method.ts:7:35) at <anonymous> (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/method.ts:47:9) at Object.<anonymous> (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/method.ts:48:28) at Module._compile (node:internal/modules/cjs/loader:1198:14) at Object.transformer (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/node_modules/.pnpm/tsx@4.15.4/node_modules/tsx/dist/register-DBk1V6ja.cjs:2:823) at Module.load (node:internal/modules/cjs/loader:1076:32) at Function.Module._load (node:internal/modules/cjs/loader:911:12) at Module.require (node:internal/modules/cjs/loader:1100:19) LOG: Exiting method 'BankService.callAPIWithError'. ----exit----- **/

Field

类型定义

/** * Context provided to a class field decorator. * @template This The type on which the class element will be defined. For a static class element, this will be * the type of the constructor. For a non-static class element, this will be the type of the instance. * @template Value The type of the decorated class field. */ interface ClassFieldDecoratorContext< This = unknown, Value = unknown, > { /** The kind of class element that was decorated. */ readonly kind: "field"; /** The name of the decorated class element. */ readonly name: string | symbol; /** A value indicating whether the class element is a static (`true`) or instance (`false`) element. */ readonly static: boolean; /** A value indicating whether the class element has a private name. */ readonly private: boolean; /** An object that can be used to access the current value of the class element at runtime. */ readonly access: { /** * Determines whether an object has a property with the same name as the decorated element. */ has(object: This): boolean; /** * Gets the value of the field on the provided object. */ get(object: This): Value; /** * Sets the value of the field on the provided object. */ set(object: This, value: Value): void; }; /** * Adds a callback to be invoked either before static initializers are run (when * decorating a `static` element), or before instance initializers are run (when * decorating a non-`static` element). */ addInitializer(initializer: (this: This) => void): void; readonly metadata: DecoratorMetadata; }

示例

function fieldD(val: any, context: ClassFieldDecoratorContext) { return function (initVal: any) { console.log(initVal, context.name); return initVal + 1; }; } class A { @fieldD name: string = "name"; @fieldD age: number = 18; @fieldD static staticName: string = "staticName"; } const a = new A(); console.log(a); /** Console staticName staticName name name 18 age A { name: 'name1', age: 19 } **/

Accesscor

类装饰器引入了一个新命令accessor
class C { accessor x = 1; }
它其实是一种语法糖,相当于声明属性x是私有属性#x的存取接口。上面的代码等同于下面的代码。
class C { #x = 1; get x() { return this.#x; } set x(val) { this.#x = val; } }

类型定义

/** * Context provided to a class `accessor` field decorator. * @template This The type on which the class element will be defined. For a static class element, this will be * the type of the constructor. For a non-static class element, this will be the type of the instance. * @template Value The type of decorated class field. */ interface ClassAccessorDecoratorContext< This = unknown, Value = unknown, > { /** The kind of class element that was decorated. */ readonly kind: "accessor"; /** The name of the decorated class element. */ readonly name: string | symbol; /** A value indicating whether the class element is a static (`true`) or instance (`false`) element. */ readonly static: boolean; /** A value indicating whether the class element has a private name. */ readonly private: boolean; /** An object that can be used to access the current value of the class element at runtime. */ readonly access: { /** * Determines whether an object has a property with the same name as the decorated element. */ has(object: This): boolean; /** * Invokes the getter on the provided object. * * @example * let value = context.access.get(instance); */ get(object: This): Value; /** * Invokes the setter on the provided object. * * @example * context.access.set(instance, value); */ set(object: This, value: Value): void; }; /** * Adds a callback to be invoked either before static initializers are run (when * decorating a `static` element), or before instance initializers are run (when * decorating a non-`static` element). */ addInitializer(initializer: (this: This) => void): void; readonly metadata: DecoratorMetadata; }
 

示例

function readonly<This, Return = number>( target: ClassAccessorDecoratorTarget<This, Return>, context: ClassAccessorDecoratorContext<This, Return>, ) { const result: ClassAccessorDecoratorResult<This, Return> = { get(this: This) { return target.get.call(this); }, set(value: Return) { throw new Error( `Cannot assign to read-only property '${String(context.name)}'.`, ); }, init(this: This, value: Return) { console.log("init value", value); return value; }, }; return result; } class C { @readonly accessor x: number = 1; } const c = new C(); console.log(c.x); c.x = 10; console.log(c.x); /** Console init value 1 1 /Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/accessor.ts:10 throw new Error( ^ Error: Cannot assign to read-only property 'x'. at _C.set (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/accessor.ts:10:13) at <anonymous> (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/accessor.ts:31:3) at Object.<anonymous> (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/demos/accessor.ts:32:16) at Module._compile (node:internal/modules/cjs/loader:1198:14) at Object.transformer (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/node_modules/.pnpm/tsx@4.15.4/node_modules/tsx/dist/register-DBk1V6ja.cjs:2:823) at Module.load (node:internal/modules/cjs/loader:1076:32) at Function.Module._load (node:internal/modules/cjs/loader:911:12) at Module.require (node:internal/modules/cjs/loader:1100:19) at require (node:internal/modules/cjs/helpers:119:18) at <anonymous> (/Users/wuchanghong/WebstormProjects/typescript-decorators-demo/src/index.ts:5:8) **/

Getter/Setter

类型定义

/** * Context provided to a class getter decorator. * @template This The type on which the class element will be defined. For a static class element, this will be * the type of the constructor. For a non-static class element, this will be the type of the instance. * @template Value The property type of the decorated class getter. */ interface ClassGetterDecoratorContext< This = unknown, Value = unknown, > { /** The kind of class element that was decorated. */ readonly kind: "getter"; /** The name of the decorated class element. */ readonly name: string | symbol; /** A value indicating whether the class element is a static (`true`) or instance (`false`) element. */ readonly static: boolean; /** A value indicating whether the class element has a private name. */ readonly private: boolean; /** An object that can be used to access the current value of the class element at runtime. */ readonly access: { /** * Determines whether an object has a property with the same name as the decorated element. */ has(object: This): boolean; /** * Invokes the getter on the provided object. * * @example * let value = context.access.get(instance); */ get(object: This): Value; }; /** * Adds a callback to be invoked either before static initializers are run (when * decorating a `static` element), or before instance initializers are run (when * decorating a non-`static` element). */ addInitializer(initializer: (this: This) => void): void; readonly metadata: DecoratorMetadata; } /** * Context provided to a class setter decorator. * @template This The type on which the class element will be defined. For a static class element, this will be * the type of the constructor. For a non-static class element, this will be the type of the instance. * @template Value The type of the decorated class setter. */ interface ClassSetterDecoratorContext< This = unknown, Value = unknown, > { /** The kind of class element that was decorated. */ readonly kind: "setter"; /** The name of the decorated class element. */ readonly name: string | symbol; /** A value indicating whether the class element is a static (`true`) or instance (`false`) element. */ readonly static: boolean; /** A value indicating whether the class element has a private name. */ readonly private: boolean; /** An object that can be used to access the current value of the class element at runtime. */ readonly access: { /** * Determines whether an object has a property with the same name as the decorated element. */ has(object: This): boolean; /** * Invokes the setter on the provided object. * * @example * context.access.set(instance, value); */ set(object: This, value: Value): void; }; /** * Adds a callback to be invoked either before static initializers are run (when * decorating a `static` element), or before instance initializers are run (when * decorating a non-`static` element). */ addInitializer(initializer: (this: This) => void): void; readonly metadata: DecoratorMetadata; }

示例

以下示例中 getter/setter 都将原始值 * 2,并打印出相关过程
function doubleGetter(value: any, context: ClassGetterDecoratorContext) { // console.log(value, context); return function (this: any) { const result = value.call(this) * 2; console.log( "[doubleGetter]", "originalValue:", value.call(this), "doubleValue:", result, ); return result; }; } function doubleSetter(value: any, context: ClassSetterDecoratorContext) { // console.log(value, context); return function (this: any, val: number) { const doubleValue = val * 2; console.log( "[doubleSetter]", "originalValue:", val, "doubleValue:", doubleValue, ); return value.call(this, doubleValue); }; } class Counter { _num: number; constructor() { this._num = 0; } @doubleGetter get num() { return this._num; } @doubleSetter set num(val: number) { this._num = val; } } const counter = new Counter(); counter.num = 1; console.log(counter.num); /** Console [doubleSetter] originalValue: 1 doubleValue: 2 [doubleGetter] originalValue: 2 doubleValue: 4 4 **/
 
以上的 demo 代码放在 github 上:

参考