Strip Typescript decorator in release build

Viewed 1718

I have debug-time Typescript decorator @log, which logs input/output/stats of the decorated functions.

I'd like to totally strip this particular @log decorator when compiling release version.

It is easy to remove console.log statements from the release build or do things conditionally in the decorator code, but I would like to make sure there's no overhead with calling decorator function itself.

Is there any way this can be achieved with Typescript?

My project is webpack-based. If this is not possible with Typescript, maybe this can be done at later stage with Babel plugin, with UglifyJS or some other alternative plugin?

2 Answers

This compile-time Transformer removes Decorators from elements and from named imports.
I'll keep the code updated, as it's just a (working) test.

export default (decorators: string[]) => {
  const importDeclarationsToRemove = [] as ts.ImportDeclaration[];

  const updateNamedImports = (node: ts.NamedImports) => {
    const newElements = node.elements.filter(v => !decorators.includes(v.name.getText()));

    if (newElements.length > 0) {
      ts.updateNamedImports(node, newElements);
    } else {
      importDeclarationsToRemove.push(node.parent.parent);
    }
  };

  const createVisitor = (
    context: ts.TransformationContext
  ): ((node: ts.Node) => ts.VisitResult<ts.Node>) => {
    const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<any> => {
      // Remove Decorators from imports
      if (ts.isNamedImports(node)) {
        updateNamedImports(node);
      }

      // Remove Decorators applied to elements
      if (ts.isDecorator(node)) {
        const decorator = node as ts.Decorator;
        const identifier = decorator.getChildAt(1) as ts.Identifier;

        if (decorators.includes(identifier.getText())) {
          return undefined;
        }
      }

      const resultNode = ts.visitEachChild(node, visitor, context);
      const index = importDeclarationsToRemove.findIndex(id => id === resultNode);

      if (index !== -1) {
        importDeclarationsToRemove.splice(index, 1);
        return undefined;
      }

      return resultNode;
    };

    return visitor;
  };

  return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile) =>
    sourceFile.fileName.endsWith('component.ts')
      ? ts.visitNode(sourceFile, createVisitor(context))
      : sourceFile;
};

program.emit(
  program.getSourceFile('test.component.ts'),
  undefined,
  undefined,
  undefined,
  {
    before: [stripDecorators(['Stateful', 'StatefulTwo', 'StatefulThree'])]
  }
);

Input:

import { Stateful, StatefulThree, StatefulTwo } from './decorators';

@Stateful
@StatefulTwo
@StatefulThree
export class Example {
  private str = '';

  getStr(): string {
    return this.str;
  }
}

JS output:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
class Example {
    constructor() {
        this.str = '';
    }
    getStr() {
        return this.str;
    }
}
exports.Example = Example;

When asking this question, I totally overlooked one embarrassingly trivial aspect. Decorator function itself is only invoked once per method declaration. If decorator is evaluated at initialization time to no-op function, overhead will only occur at initialization time and it will be pretty minimal as demonstrated in the code below.

Class instantiation and runtime function calls to the functions, marked with the @log decorator, will be free from any overhead.

const DEBUG = false;

const logDebug = function(_target: any, key: string, descriptor: PropertyDescriptor): any {
    console.log("log(): called");

    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        const functionName = key;
        console.log(functionName + "(" + args.join(", ") + ")");
        const result = originalMethod.apply(this, args);
        console.log("=> " + result);
        return result;
    };
    return descriptor;
};
const logNoop = function() {};
const log = DEBUG ? logDebug : logNoop;

class Test {
    @log
    test(a: number, b: number) {
        console.log("test(): called", a, b);
    }
}

new Test().test(1, 2);
new Test().test(3, 5);

Fragment of the compiled JS to show that overhead is indeed minimal:

var Test = /** @class */ (function () {
    function Test() {
    }
    Test.prototype.test = function (a, b) {
        console.log("test(): called", a, b);
    };
    __decorate([
        log
    ], Test.prototype, "test", null);
    return Test;
}());
Related