Skip to main content

Decorators

  • Attaching to a class: access to the class prototype and its member properties.
  • Attaching to a class property: access to the name and value of that property, along with its class prototype target.
  • Attaching to a class method parameter: access to that parameter’s index, name and value.
  • Attaching to a class method: access to the method’s parameters, the metadata associated with the method object, along with its class prototype target.
// class definitions
@decorator
class MyComponent extends React.Component<Props, State> {
// class properties
@decorator
private static api_version: string

// class method parameters
private handleFormSubmit1(@decorator myParam: string) {}

// class methods
@decorator
private handleFormSubmit2() {}

// accessors
@decorator
public myAccessor() {
return this.privateProperty
}
}

Decorators Pros

  • 实现 Open-closed 原则.
  • 分离辅助性功能逻辑 (Before/After 钩子, Trace, Log, Report, Debounce/Throttle) 与业务逻辑.
  • 抽象公有功能函数.
  • 装饰器模式是 Class 继承的一个替代模式. (类似于组合模式)

Decorators Types

type Decorator = (value: Input, context: {
kind: string
name: string | symbol
access: {
get?: () => unknown
set?: (value: unknown) => void
}
private?: boolean
static?: boolean
addInitializer: (initializer: () => void) => void
}) => Output | void

type ClassDecorator = (value: Function, context: {
kind: 'class'
name: string | undefined
addInitializer: (initializer: () => void) => void
}) => Function | void

type ClassMethodDecorator = (value: Function, context: {
kind: 'method'
name: string | symbol
access: { get: () => unknown }
static: boolean
private: boolean
addInitializer: (initializer: () => void) => void
}) => Function | void

type ClassGetterDecorator = (value: Function, context: {
kind: 'getter'
name: string | symbol
access: { get: () => unknown }
static: boolean
private: boolean
addInitializer: (initializer: () => void) => void
}) => Function | void

type ClassSetterDecorator = (value: Function, context: {
kind: 'setter'
name: string | symbol
access: { set: (value: unknown) => void }
static: boolean
private: boolean
addInitializer: (initializer: () => void) => void
}) => Function | void

type ClassFieldDecorator = (value: undefined, context: {
kind: 'field'
name: string | symbol
access: { get: () => unknown, set: (value: unknown) => void }
static: boolean
private: boolean
addInitializer: (initializer: () => void) => void
}) => (initialValue: unknown) => unknown | void

type ClassAutoAccessorDecorator = (
value: {
get: () => unknown
set: (value: unknown) => void
},
context: {
kind: 'accessor'
name: string | symbol
access: { get: () => unknown, set: (value: unknown) => void }
static: boolean
private: boolean
addInitializer: (initializer: () => void) => void
}
) => {
get?: () => unknown
set?: (value: unknown) => void
init?: (initialValue: unknown) => unknown
} | void

Class Decorators

type Constructor<T = object> = new (...args: any[]) => T

function toString<Class extends Constructor>(
Value: Class,
context: ClassDecoratorContext<Class>
) {
return class extends Value {
constructor(...args: any[]) {
super(...args)
console.log(JSON.stringify(this))
console.log(JSON.stringify(context))
}
}
}

@toString
class Person {
name: string

constructor(name: string) {
this.name = name
}

greet() {
return `Hello, ${this.name}`
}
}
const person = new Person('Simon')
/**
* Logs:
* {"name":"Simon"}
* {"kind":"class","name":"Person"}
*/

Property Decorators

function upperCase<T>(
target: undefined,
context: ClassFieldDecoratorContext<T, string>
) {
return function (this: T, value: string) {
return value.toUpperCase()
}
}

class MyClass {
@upperCase
prop1 = 'hello!'
}

console.log(new MyClass().prop1) // Logs: HELLO!

Method Decorators

function log<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Return
>
) {
const methodName = String(context.name)

function replacementMethod(this: This, ...args: Args): Return {
console.log(`LOG: Entering method '${methodName}'.`)
const result = target.call(this, ...args)
console.log(`LOG: Exiting method '${methodName}'.`)
return result
}

return replacementMethod
}

class MyClass {
@log
sayHello() {
console.log('Hello!')
}
}

new MyClass().sayHello()

Getter and Setter Decorators

function range<This, Return extends number>(min: number, max: number) {
return function (
target: (this: This) => Return,
context: ClassGetterDecoratorContext<This, Return>
) {
return function (this: This): Return {
const value = target.call(this)

if (value < min || value > max) {
throw new Error('Invalid')
}

Object.defineProperty(this, context.name, {
value,
enumerable: true,
})
return value
}
}
}

class MyClass {
private _value = 0

constructor(value: number) {
this._value = value
}

@range(1, 100)
get getValue(): number {
return this._value
}
}

const obj = new MyClass(10)
console.log(obj.getValue) // Valid: 10

const obj2 = new MyClass(999)
console.log(obj2.getValue) // Throw: Invalid!

Reflect Metadata

IoC and DI implementation:

const INJECTIONS = new WeakMap()

function createInjections() {
const injections = []

function injectable(Class) {
INJECTIONS.set(Class, injections)
}

function inject(injectionKey) {
return function applyInjection(v, context) {
injections.push({ injectionKey, set: context.access.set })
}
}

return { injectable, inject }
}

class Container {
registry = new Map()

register(injectionKey, value) {
this.registry.set(injectionKey, value)
}

lookup(injectionKey) {
this.registry.get(injectionKey)
}

create(Class) {
const instance = new Class()

for (const { injectionKey, set } of INJECTIONS.get(Class) || [])
set.call(instance, this.lookup(injectionKey))

return instance
}
}

class Store {}

const { injectable, inject } = createInjections()

@injectable
class C {
@inject('store') store
}

const container = new Container()
const store = new Store()

container.register('store', store)

const c = container.create(C)

c.store === store // true

AOP programming:

const PATH_METADATA = 'path'
const METHOD_METADATA = 'method'

function Controller(path: string): ClassDecorator {
return (target) => {
Reflect.defineMetadata(PATH_METADATA, path, target)
}
}

function createMappingDecorator(method: string) {
return (path: string): MethodDecorator => {
return (target, key, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value)
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value)
}
}
}

const Get = createMappingDecorator('GET')
const Post = createMappingDecorator('POST')

function mapRoute(instance: object) {
const prototype = Object.getPrototypeOf(instance)

// 筛选出类的 methodName
const methodsNames = Object.getOwnPropertyNames(prototype).filter(
item => !isConstructor(item) && isFunction(prototype[item])
)
return methodsNames.map((methodName) => {
const fn = prototype[methodName]

// 取出定义的 metadata
const route = Reflect.getMetadata(PATH_METADATA, fn)
const method = Reflect.getMetadata(METHOD_METADATA, fn)

return {
route,
method,
fn,
methodName,
}
})
}

@Controller('/test')
class SomeClass {
@Get('/a')
someGetMethod() {
return 'hello world'
}

@Post('/b')
somePostMethod() {}
}

Reflect.getMetadata(PATH_METADATA, SomeClass) // '/test'

mapRoute(new SomeClass())
/**
* [{
* route: '/a',
* method: 'GET',
* fn: someGetMethod() { ... },
* methodName: 'someGetMethod'
* },{
* route: '/b',
* method: 'POST',
* fn: somePostMethod() { ... },
* methodName: 'somePostMethod'
* }]
*
*/