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