The following questions are from the discussion with company partners and net friends, sorted into chapters, hoping to provide another way of thinking (to avoid stepping on the pit) to solve the problem.

Decorator

Decorators are nothing new. In TypeScript 1.5 +, we can write decorators faster with built-in types ClassDecorator, PropertyDecorator, MethodDecorator, and ParameterDecorator. As MethodDecorator:

declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) = > TypedPropertyDescriptor<T> | void;
Copy the code

When used, simply add type annotations where appropriate, and the parameter types of anonymous functions are automatically derived.

function methodDecorator () :MethodDecorator {
  return (target, key, descriptor) = > {
    // ...
  };
}
Copy the code

It’s worth noting that TypeScript doesn’t know about this when you add attributes to the prototype of the target class in the Decorator:

function testAble() :ClassDecorator {
  return target= > {
    target.prototype.someValue = true}}@testAble(a)class SomeClass {}

const someClass = new SomeClass()

someClass.someValue() // Error: Property 'someValue' does not exist on type 'SomeClass'.
Copy the code

This is common, especially if you want to use a Decorator to extend a class.

There is an issue on GitHub about this, and so far, there is no suitable solution to implement it. The main problem is that TypeScript doesn’t know whether the target class uses a Decorator or not, and the name of the Decorator. From the perspective of this issue, the recommended solution is to use mixins:

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

// mixin function declaration, need to implement
declare function mixin<T1.T2> (. MixIns: [Constructor
       
        , Constructor
        
         ]
        
       ) :Constructor<T1 & T2>;

class MixInClass1 {
    mixinMethod1() {}
}

class MixInClass2 {
    mixinMethod2() {}
}

class Base extends mixin(MixInClass1, MixInClass2) {
    baseMethod() { }
}

const x = new Base();

x.baseMethod(); // OK
x.mixinMethod1(); // OK
x.mixinMethod2(); // OK
x.mixinMethod3(); // Error
Copy the code

When refactoring a large number of JavaScript decorators into mixins, this can be a daunting task.

Here are some tricks to make the transition from JavaScript to TypeScript easier:

  • An explicitly assigned assertion modifier, that is, in a class, explicitly states that certain attributes exist on the class:

    function testAble() :ClassDecorator {
      return target= > {
        target.prototype.someValue = true}}@testAble(a)class SomeClass {
      publicsomeValue! :boolean;
    }
    
    const someClass = new SomeClass();
    someClass.someValue // true
    Copy the code
  • In the Decorator, define a separate interface with the type of the attribute extended by the Decorator:

    interface SomeClass {
      someValue: boolean;
    }
    
    function testAble() :ClassDecorator {
      return target= > {
        target.prototype.someValue = true}}@testAble(a)class SomeClass {}
    
    const someClass = new SomeClass();
    someClass.someValue // true
    Copy the code

Reflect Metadata

Reflect Metadata is a proposal in ES7 for adding and reading Metadata at declaration time. TypeScript already supports TypeScript in version 1.5+. You just need to:

  • npm i reflect-metadata --save.
  • intsconfig.jsonIn the configurationemitDecoratorMetadataOptions.

It has many usage scenarios.

Getting type information

For example, in VUE property-decorator 6.1 and below, a Prop decorator can get the property type to be passed to the VUE by using the Reflect.getMetadata API as follows:

function Prop() :PropertyDecorator {
  return (target, key: string) = > {
    const type = Reflect.getMetadata('design:type', target, key);
    console.log(`${key} type: ${type.name}`);
    // other...}}class SomeClass {
  @Prop(a)publicAprop! :string;
};
Copy the code

The running code is visible on the console at Aprop Type: String. In addition to being able to get the property type, GetMetadata (” Design: Paramtypes “, target, key) and reflect. getMetadata(“design: returnType “, target, Key) can get the function parameter type and return value type, respectively.

The custommetadataKey

In addition to retrieving type information, it is often used to customize the metadataKey and retrieve its value when appropriate, as shown in the following example:

function classDecorator() :ClassDecorator {
  return target= > {
    // Define metadata on the class with key as' classMetaData 'and value as' a'
    Reflect.defineMetadata('classMetaData'.'a', target); }}function methodDecorator() :MethodDecorator {
  return (target, key, descriptor) = > {
    // Define the metadata on the class's prototype property 'someMethod' with key 'methodMetaData' and value 'b'
    Reflect.defineMetadata('methodMetaData'.'b', target, key); }}@classDecorator(a)class SomeClass {

  @methodDecorator()
  someMethod() {}
};

Reflect.getMetadata('classMetaData', SomeClass);                         // 'a'
Reflect.getMetadata('methodMetaData'.new SomeClass(), 'someMethod');    // 'b'
Copy the code

Use cases

Inversion of control and dependency injection

Inversion of control and dependency injection are implemented in versions of Angular 2+. Now let’s implement a simpler version:

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

const Injectable = (): ClassDecorator= > target => {}

class OtherService {
  a = 1
}

@Injectable(a)class TestService {
  constructor(public readonly otherService: OtherService) {}

  testMethod() {
    console.log(this.otherService.a); }}const Factory = <T>(target: Constructor<T>): T= > {
  // Get all the injected services
  const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
  const args = providers.map((provider: Constructor) = > new provider());
  return newtarget(... args); } Factory(TestService).testMethod()/ / 1
Copy the code

Implementation of Controller and Get

If you’re developing Node applications in TypeScript, you’re familiar with decorators like Controller, Get, and POST:

@Controller('/test')
class SomeClass {

  @Get('/a')
  someGetMethod() {
    return 'hello world';
  }

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

Copy the code

They are also based on the Reflect Metadata implementation, except that this time we define the metadataKey on the descriptor value (explained later). The simple implementation is as follows:

const METHOD_METADATA = 'method';const PATH_METADATA = 'path';const Controller = (path: string) :ClassDecorator= > {
  return target= >{ Reflect.defineMetadata(PATH_METADATA, path, target); }}const createMappingDecorator = (method: string) = > (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');
Copy the code

Next, create a function that maps route:

function mapRoute(instance: Object) {
  const prototype = Object.getPrototypeOf(instance);
  
  // Filter out the class methodName
  const methodsNames = Object.getOwnPropertyNames(prototype)
                              .filter(item= >! IsConstructor (item) && isFunction (prototype (item)));return methodsNames.map(methodName= > {
    const fn = prototype[methodName];

    // Retrieve the defined metadata
    const route = Reflect.getMetadata(PATH_METADATA, fn);
    constMethod = reflect.getmetadata (METHOD_METADATA, fn);return {
      route,
      method,
      fn,
      methodName
    }
  })
};
Copy the code

We can get some useful information:

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' * }] * */
Copy the code

Finally, just tie the route information to express or KOA.

As for why we define values on descriptor, we want the argument to the mapRoute function to be an instance, not the class itself (inversion of control).

More and more

  • Working with TypeScript
  • Understand TypeScript in depth