装饰器让TypeScript的世界更好。 我们使用的许多库都基于这一强大特性构建, 例如Angular和Nestjs。 在这篇博客中我将介绍装饰器和它的许多细节。 我希望在读完这篇文章后,你可以理解何时和如何使用这一强的的特性。
概览
装饰器本质上是一种特殊的函数被应用在于:
- 类
- 类属性
- 类方法
- 类访问器
- 类方法的参数
因此,应用装饰器很像组合函数链,非常类似于高阶函数或类。 使用装饰器,我们可以轻松实现代理模式来减少代码并做一些很酷的事情。
装饰器的语法十分简单,只需要在想使用的装饰器前加上@符号,装饰器就会被应用到目标上:
function simpleDecorator() {
console.log('---hi I am a decorator---')
}
@simpleDecorator
class A {}
一共有5种装饰器可被我们使用:
- 类装饰器
- 属性装饰器
- 方法装饰器
- 访问器装饰器
- 参数装饰器
让我们来快速认识一下这五种装饰器:
@classDecorator
class Bird {
@propertyDecorator
name: string;
@methodDecorator
fly(
@parameterDecorator
meters: number
) {}
@accessorDecorator
get egg() {}
}
执行
时机
在程序运行时,装饰器只会在类定义时应用一次,例如:
function f(C) {
console.log('apply decorator')
return C
}
@f
class A {}
// output: apply decorator
这里的代码会在终端中打印apply decorator,即便我们其实并没有使用类A。
执行顺序
不同类型的装饰器的执行顺序是明确定义的:
- 参数装饰器,然后是方法、访问器或属性装饰器应用于每个实例成员。
- 参数装饰器,然后是方法、访问器或属性装饰器应用于每个静态成员。
- 参数装饰器应用于构造函数。
- 类装饰器应用于类。
例如,思考以下代码:
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
@f("Class Decorator")
class C {
@f("Static Property")
static prop?: number;
@f("Static Method")
static method(@f("Static Method Parameter") foo) {}
constructor(@f("Constructor Parameter") foo) {}
@f("Instance Method")
method(@f("Instance Method Parameter") foo) {}
@f("Instance Property")
prop?: number;
}
它将会打印出以下信息:
evaluate: Instance Method
evaluate: Instance Method Parameter
call: Instance Method Parameter
call: Instance Method
evaluate: Instance Property
call: Instance Property
evaluate: Static Property
call: Static Property
evaluate: Static Method
evaluate: Static Method Parameter
call: Static Method Parameter
call: Static Method
evaluate: Class Decorator
evaluate: Constructor Parameter
call: Constructor Parameter
call: Class Decorator
你也许会注意到执行实例属性prop晚于实例方法method 然而执行静态属性static prop早于静态方法static method。 这是因为对于属性/方法/访问器装饰器而言,执行顺序取决于声明它们的顺序。
然而,同一方法中不同参数的装饰器的执行顺序是相反的, 最后一个参数的装饰器会最先被执行:
function f(key: string): any {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
method(
@f("Parameter Foo") foo,
@f("Parameter Bar") bar
) {}
}
这里的代码打印出的结果为:
evaluate: Parameter Foo
evaluate: Parameter Bar
call: Parameter Bar
call: Parameter Foo
多个装饰器的组合
你可以对同一目标应用多个装饰器。它们的组合顺序为:
- 求值外层装饰器
- 求值内层装饰器
- 调用内层装饰器
- 调用外层装饰器
例如:
function f(key: string) {
console.log("evaluate: ", key);
return function () {
console.log("call: ", key);
};
}
class C {
@f("Outer Method")
@f("Inner Method")
method() {}
}
这里的代码打印出的结果为:
evaluate: Outer Method
evaluate: Inner Method
call: Inner Method
call: Outer Method
定义
类装饰器
类型声明:
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
- @Params:
- target:类的构造函数
- @Returns:如果类装饰器返回一个值,它将替换类声明。
因此,类装饰器适合用于继承一个现有类并添加一些属性和方法。
例如我们可以添加一个toString方法给所有的类来覆盖它原有的toString方法。
type Consturctor = { new (...args: any[]): any };
function toString<T extends Consturctor>(BaseClass: T) {
return class extends BaseClass {
toString() {
return JSON.stringify(this);
}
};
}
@toString
class C {
public foo = "foo";
public num = 24;
}
console.log(new C().toString())
// -> {"foo":"foo","num":24}
遗憾的是我们无法定义类型安全的装饰器,这意味着:
declare function Blah<T>(target: T): T & {foo: number}
@Blah
class Foo {
bar() {
return this.foo; // Property 'foo' does not exist on type 'Foo'
}
}
new Foo().foo; // Property 'foo' does not exist on type 'Foo'
这是 Typescript 中的一个众所周知的问题。 我们现在可以做的是提供一个带有类型信息的类,以便由目标类扩展:
declare function Blah<T>(target: T): T & {foo: number}
class Base {
foo: number;
}
@Blah
class Foo extends Base {
bar() {
return this.foo;
}
}
new Foo().foo;
属性装饰器
类型声明:
type PropertyDecorator =
(target: Object, propertyKey: string | symbol) => void;
- @Params:
- target:静态成员的类的构造函数或实例成员的类的原型。
- propertyKey:属性的名称。
- @Returns:返回值将被忽略。
除了用于收集信息外,属性装饰器也可以用来给类添加额外的方法和属性。 例如我们可以写一个装饰器来给某些属性添加监听器。
function capitalizeFirstLetter(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function observable(target: any, key: string): any {
// prop -> onPropChange
const targetKey = "on" + capitalizeFirstLetter(key) + "Change";
target[targetKey] =
function (fn: (prev: any, next: any) => void) {
let prev = this[key];
Reflect.defineProperty(this, key, {
set(next) {
fn(prev, next);
prev = next;
}
})
};
}
class C {
@observable
foo = -1;
@observable
bar = "bar";
}
const c = new C();
c.onFooChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`))
c.onBarChange((prev, next) => console.log(`prev: ${prev}, next: ${next}`))
c.foo = 100; // -> prev: -1, next: 100
c.foo = -3.14; // -> prev: 100, next: -3.14
c.bar = "baz"; // -> prev: bar, next: baz
c.bar = "sing"; // -> prev: baz, next: sing
方法装饰器
类型声明:
type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
- @Params:
- target:静态成员的类的构造函数,或实例成员的类的原型。
- propertyKey:属性的名称。
- descriptor:成员的属性描述符。
- @Returns:如果返回一个值,它将被用作成员的描述符。
方法装饰器不同于属性装饰器的地方在于descriptor参数。 通过这个参数我们可以修改方法原本的实现,添加一些共用逻辑。 例如我们可以给一些方法添加打印输入与输出的能力:
function logger(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log('params: ', ...args);
const result = original.call(this, ...args);
console.log('result: ', result);
return result;
}
}
class C {
@logger
add(x: number, y:number ) {
return x + y;
}
}
const c = new C();
c.add(1, 2);
// -> params: 1, 2
// -> result: 3
访问器装饰器
访问器装饰器总体上讲和方法装饰器很接近,唯一的区别在于描述器中有的key不同:
方法装饰器的描述器的key为:
- value
- writable
- enumerable
- configurable
访问器装饰器的描述器的key为:
- get
- set
- enumerable
- configurable
例如,我们可以将某个属性设为不可变值:
function immutable(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.set;
descriptor.set = function (value: any) {
return original.call(this, { ...value })
}
}
class C {
private _point = { x: 0, y: 0 }
@immutable
set point(value: { x: number, y: number }) {
this._point = value;
}
get point() {
return this._point;
}
}
const c = new C();
const point = { x: 1, y: 1 }
c.point = point;
console.log(c.point === point)
// -> false
参数装饰器
类型声明:
type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
- @Params:
- target:静态成员的类的构造函数,或实例成员的类的原型。
- propertyKey:属性的名称(方法的名称,而不是参数的名称)。
- parameterIndex:函数参数列表中参数的顺序索引。
- @Returns:返回值将被忽略。
单独的参数装饰器能做的事情很有限,它一般都被用于记录可被其它装饰器使用的信息。
结合
对于一些复杂场景, 我们可能需要结合使用不同的装饰器。 例如如果我们不仅想给我们的接口添加静态检查,还想加上运行时检查的能力。
我们可以用3个步骤来实现这个功能:
- 标记需要检查的参数 (因为参数装饰器先于方法装饰器执行)。
- 更改方法描述符的值,在方法之前运行参数验证器,如果失败则抛出错误。
- 运行原来的方法。
以下是代码:
type Validator = (x: any) => boolean;
// save the marks
const validateMap: Record<string, Validator[]> = {};
// 1. mark the parameters need to be validated
function typedDecoratorFactory(validator: Validator): ParameterDecorator {
return (_, key, index) => {
const target = validateMap[key as string] ?? [];
target[index] = validator;
validateMap[key as string] = target;
}
}
function validate(_: Object, key: string, descriptor: PropertyDescriptor) {
const originalFn = descriptor.value;
descriptor.value = function(...args: any[]) {
// 2. run the validators
const validatorList = validateMap[key];
if (validatorList) {
args.forEach((arg, index) => {
const validator = validatorList[index];
if (!validator) return;
const result = validator(arg);
if (!result) {
throw new Error(
`Failed for parameter: ${arg} of the index: ${index}`
);
}
});
}
// 3. run the original method
return originalFn.call(this, ...args);
}
}
const isInt = typedDecoratorFactory((x) => Number.isInteger(x));
const isString = typedDecoratorFactory((x) => typeof x === 'string');
class C {
@validate
sayRepeat(@isString word: string, @isInt x: number) {
return Array(x).fill(word).join('');
}
}
const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error
正如例子中展示的, 对我们来说同时理解不同种类装饰器的执行顺序和职责都很重要。
元数据
严格地说,元数据和装饰器是 ECMAScript 的两个独立部分。 然而,如果你想实现像是反射这样的能力,你总是同时需要它们。
如果我们回顾上一个例子,如果我们不想写各种不同的检查器呢? 或者说,能否只写一个检查器能够通过我们编写的TS类型声明来自动运行类型检查?
有了reflect-metadata的帮助, 我们可以获取编译期的类型。
import 'reflect-metadata';
function validate(
target: Object,
key: string,
descriptor: PropertyDescriptor
) {
const originalFn = descriptor.value;
// get the design type of the parameters
const designParamTypes = Reflect
.getMetadata('design:paramtypes', target, key);
descriptor.value = function (...args: any[]) {
args.forEach((arg, index) => {
const paramType = designParamTypes[index];
const result = arg.constructor === paramType
|| arg instanceof paramType;
if (!result) {
throw new Error(
`Failed for validating parameter: ${arg} of the index: ${index}`
);
}
});
return originalFn.call(this, ...args);
}
}
class C {
@validate
sayRepeat(word: string, x: number) {
return Array(x).fill(word).join('');
}
}
const c = new C();
c.sayRepeat('hello', 2); // pass
c.sayRepeat('', 'lol' as any); // throw an error
目前为止一共有三种编译期类型可以拿到:
- design:type,属性的类型
- desin:paramtypes,方法参数的类型
- design:returntype,方法返回值类型的类型
这三种方式拿到的结果都是构造函数(例如String和Number)。规则是:
- number -> Number
- string -> String
- boolean -> Boolean
- void/null/never -> undefined
- Array/Tuple -> Array
- Class -> The constructor function of the class
- Enum -> 纯数字枚举或对象时的数字
- Function -> Function
- 其他是 Object
何时使用?
现在我们可以对于何时使用装饰器得出结论, 在阅读上面的代码中你可能也有所感觉。
我将例举一些常用的使用场景:
- Before/After钩子。
- 监听属性改变或者方法调用。
- 对方法的参数做转换。
- 添加额外的方法和属性。
- 运行时类型检查。
- 自动序列化和反序列化。
- 依赖注入。
我希望读完这篇文章后,你可以找到装饰器的更多使用场景,并且用它来简化你的代码。
以上是对 mirone.me/a-complete-… 的一些简单翻译,希望对大家学些 ts 的装饰器有帮助