Search code examples
typescripttypescript-decorator

How to combine multiple property decorators in Typescript?


I have a class Template with a property _id which has decorators from class-transformer and typed-graphql

import {classToPlain, Exclude, Expose, plainToClass, Type } from 'class-transformer';
import { ExposeToGraphQL } from '../../decorators/exposeToGraphQL';
import { Field, ID, MiddlewareInterface, NextFn, ObjectType, ResolverData } from 'type-graphql';
import { getClassForDocument, InstanceType, prop, Typegoose } from 'typegoose';
/**
  * Class
  * @extends Typegoose
  */
@Exclude()
@ObjectType()
class Template extends Typegoose {
  // @Expose and @Type should be both covered by ExposeToGraphQL
  // @Expose()
  @Type(() => String)
  @ExposeToGraphQL()
  @Field(() => ID)
  public _id?: mongoose.Types.ObjectId;
}

Now I try to combine those two into a new custom property decorator:

/**
 *
 */
import { Expose } from 'class-transformer';
import 'reflect-metadata';

const formatMetadataKey: Symbol = Symbol('ExposeToGraphQL');

function ExposeToGraphQL() {
  console.log('ExposeToGraphQL');

  return Expose();
}

function getExposeToGraphQL(target: any, propertyKey: string) {
  console.log('getExposeToGraphQL');

  return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

export {
  ExposeToGraphQL,
  getExposeToGraphQL,
};

The custom decorator works if I only return the result of Expose(), but I don't know how to combine @Expose and @Type in @ExposeToGraphQL().


Solution

  • import { Expose, Type, TypeOptions, ExposeOptions } from 'class-transformer';
    
    /**
     * Combines @Expose then @Types decorators.
     * @param exposeOptions options that passes to @Expose()
     * @param typeFunction options that passes to @Type()
     */
    function ExposeToGraphQL(exposeOptions?: ExposeOptions, typeFunction?: (type?: TypeOptions) => Function) {
      const exposeFn = Expose(exposeOptions);
      const typeFn = Type(typeFunction);
    
      return function (target: any, key: string) {
        typeFn(target, key);
        exposeFn(target, key);
      }
    }
    

    Then you can use that decorator as follow:

    class Template extends Typegoose {
        @ExposeToGraphQL(/*exposeOptions*/ undefined, /*typeFunction*/ () => String)
        @Field(() => ID)
        public _id?: mongoose.Types.ObjectId;
    }
    

    You can find official documentation for decorator in this link.

    @Expose and @Type() are basically Decorator Factories. The main purpose of a decorator factory:

    • it return a function
    • that function will be called at runtime (right after the class, in this case is Template, was defined) with 2 arguments:
      • class prototype (Template.prototype)
      • name of the property which the decorator attached to (_id).

    If two or more decorators are attached to a same property (called as Decorator Composition), they are evaluated as follow:

    • The factory functions are executed in the same order as they are written in code
    • The functions returned by factory functions are executed in reversed order