Reflect & Decorator
WTF is Reflect ?
Let's talk about Proxy first
Proxy
The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

Proxy - validate
const validator = {
set(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
obj[prop] = value;
return true;
}
};
const person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = 'young'; // Throws an exception
person.age = 300; // Throws an exception
Proxy - manipulating DOM
function controlInput(controller, inputEl) {
return new Proxy(controller, {
set(obj, prop, value) {
if (prop === 'value') {
inputEl.value = value;
}
obj[prop] = value;
return true;
}
});
}
const controller = controlInput({ value: '' }, document.querySelect('input'));
Proxy - observer
function observe(o, callback) {
return new Proxy(o, {
set(target, property, value) {
callback(property, value);
target[property] = value;
},
});
}
const door = { open: false };
const doorObserver = observe(door, (property, value) => {
if (property === 'open') {
// ...
}
});
doorObserver.open = true;
Reflect
Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers.
Reflect is not a function object, so it's not constructible.

Why we need Reflect ?
Some Proxy bug
The scope of this
const target = {
get foo() {
return this.bar;
},
bar: 3
};
const handler = {
get(target, prop, value) {
if (prop === 'bar') return 2;
console.log('Reflect.get ', Reflect.get(target, prop, value));
console.log('target[prop] ', target[prop]);
}
};
const obj = new Proxy(target, handler);
console.log(obj.bar);
// 2
obj.foo;
// Reflect.get 2
// target[propertyKey] 3
Decorator

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) =>
TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (
target: Object,
propertyKey: string | symbol,
parameterIndex: number
) => void;
Method Decorator
function logMethod(
target: Object,
property: string,
propertyDesciptor: PropertyDescriptor
): PropertyDescriptor {
const method = propertyDesciptor.value;
propertyDesciptor.value = function (...args: any[]) {
const result = method.apply(this, args);
const params = args.map(a => JSON.stringify(a)).join(', ');
const r = JSON.stringify(result);
console.log(`Call: ${property}(${params}) => ${r}`);
return result;
}
return propertyDesciptor;
};
class Person {
constructor(
private firstName: string,
private lastName: string
) { }
@logMethod
greet(message: string): string {
return `${this.firstName} ${this.lastName} says: ${message}`;
}
}
const person = new Person('Abc', 'Def');
person.greet('hello');
// Call: greet("hello") => "Abc Def says: hello"
Property Decorator - 1
function logProperty() {
return function (target: Object, property: string) {
let _val = Reflect.get(target, property);
const getter = () => {
console.log(`Get: ${property} => ${_val}`);
return _val;
};
const setter = value => {
console.log(`Set: ${property} => ${value}`);
_val = value;
};
if (delete target[property]) {
Reflect.defineProperty(target, property, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
}
}
Property Decorator - 2
class Person {
constructor(name: string) {
this.name = name;
}
@logProperty()
name: string;
}
const person = new Person('Abc');
// Set: name => Abc
console.log(person.name);
// Get: name => Abc
// Abc
person.name = 'Def';
// Set: name => Def
Parameter Decorator - 1
const getRequiredMetadataKey = (property: string) => `required_${property}`;
function required(target: Object, property: string, parameterIndex: number) {
const requiredMetadataKey = getRequiredMetadataKey(property);
const existingRequiredParameters: number[] = (
Reflect.get(target, requiredMetadataKey) || []
);
existingRequiredParameters.push(parameterIndex);
Reflect.set(target, requiredMetadataKey, existingRequiredParameters);
}
function validate() {
return (target: any, property: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
const requiredMetadataKey = getRequiredMetadataKey(property);
const requiredParameters: number[] = Reflect.get(target, requiredMetadataKey);
descriptor.value = function () {
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (
parameterIndex >= arguments.length ||
arguments[parameterIndex] === undefined
) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
}
return descriptor;
}
}
Parameter Decorator - 2
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@validate()
greet(@required name?: string) {
console.log("Hello " + name + ", " + this.greeting);
}
}
const greeter = new Greeter('abc');
greeter.greet('GGG');
// Hello GGG, abc
greeter.greet();
// Uncaught Error: Missing required argument.
Class Decorator
function logClass(target: Function) {
const original = target;
const f: any = function (...args) {
console.log(`New: ${original['name']} is created`);
return Reflect.construct(original, args);
}
f.prototype = original.prototype;
return f;
}
@logClass
class Person {
}
const person = new Person();
// New: Person is created
console.log(person instanceof Person);
// true
Reflect metadata
Allows us to do runtime reflection on types.
namespace Reflect {
metadata(k, v): (target, property?) => void
defineMetadata(k, v, o, p?): void
hasMetadata(k, o, p?): boolean
hasOwnMetadata(k, o, p?): boolean
getMetadata(k, o, p?): any
getOwnMetadata(k, o, p?): any
getMetadataKeys(o, p?): any[]
getOwnMetadataKeys(o, p?): any[]
deleteMetadata(k, o, p?): boolean
}
import "reflect-metadata";
@Reflect.metadata("inClass", "A")
class Test {
@Reflect.metadata("inMethod", "B")
public hello(): string {
return "hello world";
}
}
console.log(Reflect.getMetadata("inClass", Test));
// 'A'
console.log(Reflect.getMetadata("inMethod", new Test(), "hello"));
// 'B'
A | new A() | A.prototype | |
---|---|---|---|
A | 'A' | undefined | undefined |
A hello | undefined | 'hello' | 'hello' |
own A | 'A' | undefined | undefined |
own A hello | undefined | undefined | 'hello' |
@Reflect.metadata('name', 'A')
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const res = [A, new A(), A.prototype].map(obj => [
Reflect.getMetadata('name', obj),
Reflect.getMetadata('name', obj, 'hello'),
Reflect.getOwnMetadata('name', obj),
Reflect.getOwnMetadata('name', obj ,'hello')
])
class A {
@Reflect.metadata('name', 'hello')
hello() {}
}
const t1 = new A()
const t2 = new A()
Reflect.defineMetadata('otherName', 'world', t2, 'hello')
Reflect.getMetadata('name', t1, 'hello') // 'hello'
Reflect.getMetadata('name', t2, 'hello') // 'hello'
Reflect.getMetadata('otherName', t2, 'hello') // 'world'
Reflect.getOwnMetadata('name', t2, 'hello') // undefined
Reflect.getOwnMetadata('otherName', t2, 'hello') // 'world'
function Prop(): PropertyDecorator {
return (target, key: string) => {
const type = Reflect.getMetadata("design:type", target, key);
console.log(`${key} type: ${type.name}`);
};
}
function Method(): MethodDecorator {
return (target, key: string) => {
const paramtype = Reflect.getMetadata("design:paramtypes", target, key);
const returntype = Reflect.getMetadata("design:returntype", target, key);
console.log(`${key} paramtype: ${paramtype.map(p => p.name).join(", ")}`);
console.log(`${key} returntype: ${returntype.name}`);
};
}
class Test2 {
@Prop()
public Aprop!: string;
@Method()
public BMethod(b: string, c: number): number {
this.Aprop = b;
return c;
}
}
// Aprop type: String
// BMethod paramtype: String, Number
// BMethod returntype: Number
Demo
Express Controller
import { Request } from 'express';
import { Controller, Get, Post } from './core';
@Controller('/test')
export class TestController {
@Get()
async getAll() {
return 'TEST !!!';
}
@Get('/a')
async getA() {
return 'WTF A';
}
@Post('/b')
async setB(req: Request) {
return `Request Body is ${JSON.stringify(req.body)}`;
}
}
export const METHOD_METADATA = 'method';
export const PATH_METADATA = 'path';
import 'reflect-metadata';
import { PATH_METADATA, METHOD_METADATA } from './constants';
export const Controller = (path: string): ClassDecorator => {
return target => {
Reflect.defineMetadata(PATH_METADATA, path, target);
};
};
export type HttpMethod = 'get' | 'post';
const createHttpMethodDecorator = (method: HttpMethod) => (path?: string): MethodDecorator => {
return (_, __, descriptor) => {
Reflect.defineMetadata(PATH_METADATA, path || '', descriptor.value!);
Reflect.defineMetadata(METHOD_METADATA, method, descriptor.value!);
return descriptor;
};
};
export const Get = createHttpMethodDecorator('get');
export const Post = createHttpMethodDecorator('post');

import { ApplicationFactory } from './core';
import { TestController } from './test.controller';
async function bootstrap() {
const app = ApplicationFactory.create([TestController]);
await app.listen(3000);
}
bootstrap();
Q & A
Reflect & Decorator
By jjaayy
Reflect & Decorator
- 428