Metadata retention with Typescript, Babel 7, Decorators Metadata retention with Typescript, Babel 7, Decorators typescript typescript

Metadata retention with Typescript, Babel 7, Decorators


Out of the box this is not supported as is noted in another answer. We can however write a babel plugin to get it to work.

Writing the code is not very complicated, the problems come from the limitations of the information we have inside babel. Babel does not perform type checking on typescript. This means we don't have any semantic information, we just have the type annotation and what information we can derive from it. This means that our solution is by necessity very limited

Limitations:

  • If no type annotation is present we have no type to write
  • If we have a type reference, we can only use the type name, we can't check if the refence is to an interface, as type alias, a class or an enum. In practice this means that :
    • If the type is an interface or a type alias, the type name will be undefined at runtime, to avoid the undefined we can do type || Object to default to object if the type does not have a runtime value associated
    • If the type is an enum, Typescript would write Number or String in the metadata, depending on the type of the enum. Since we write the type name to the metadata, this means you will end up with the container object of the enum inside the metadata.

The type serialization can be copied from the typescript compiler itself with minimal changes (and it's really just two functions serializeTypeNode and serializeTypeList all about 150 lines of code).

For this sample class the results we get are:

declare var decorator: any;interface ISampleInterface {}enum Flags { One }class OtherClass {}type ArrayAlias = number[]class Test {    @decorator    untypedProp; // no design:type    @decorator    nrProp: number // Number as expected    @decorator    strProp: string // String as expected    @decorator    boolProp: boolean // Boolean as expected    @decorator    nrPropUndefined: number | undefined // Number as expected    @decorator    strPropUndefined: string | undefined // String as expected    @decorator    boolPropUndefined: boolean | undefined // Boolean as expected    @decorator    arrayProp: number[]    // Type references    @decorator    classRefProp: OtherClass; // OtherClass || Object  = Object since OtherClass is still a class at runtime    @decorator    interfaceRefProp: ISampleInterface;  // ISampleInterface || Object  = Object since ISampleInterface is undefined at runtime    @decorator    enumRefProp: Flags; // Flags || Object = Flags since Flags exists as a value at runtime, here TS would have written Number/String    @decorator    typeAliasProp: ArrayAlias; // ArrayAlias || Object = Object since ArrayAlias does not exist t runtime and in Babel swe have no idea ArrayAlias is actually an array    @decorator    selfClassRefProp: Test; // Test || Object  = Object since Babel puts decorators instantiation before class definition, this is a quirk, this may be fixable}

The actual plugin code is not very large (minus the methods copied over from the TS version):

export default declare((api: typeof import('@babel/core'), { jsxPragma = "React" }): PluginObj => {  api.assertVersion(7);  return {    name: "transform-typescript-decorator-metadata",    inherits: syntaxTypeScript,    visitor: {      ClassDeclaration(path) {        var node = path.node;        for (const field of node.body.body) {          if (field.type !== "ClassProperty") continue;          if (field.typeAnnotation && field.typeAnnotation.type === "TSTypeAnnotation" && field.decorators && field.decorators.length > 0) {            const key = field.key as t.Identifier;            const serializedType = serializeTypeNode(field.typeAnnotation.typeAnnotation);            field.decorators.push(decorator(              t.callExpression(                t.memberExpression(t.identifier("Reflect"), t.identifier("metadata")), [                  t.stringLiteral("design:type"),                  t.logicalExpression("||", serializedType, createIdentifier("Object"))                ])            ))          }        }      },    }  };});

You can find the full plugin code, and a working sample here

Just as a side not, plugin ordering matters, if @babel/plugin-proposal-class-properties comes before our plugin it will have erased all the properties and our plugin will not have the info to emit the decorators anymore. This is the .babelrc I tested with and it works, I did not get it to with with any other configuration (but I can't say I tried that hard)

  {    "env": {},    "ignore": [],    "plugins": [      "../plugin/plugin.js",      ["@babel/plugin-proposal-decorators", { "legacy": true }],      ["@babel/plugin-proposal-class-properties", { "loose": true }],      "babel-plugin-transform-es2015-modules-commonjs"    ],    "presets": [      "@babel/preset-typescript"    ]  }


Unfortunately I'm reasonably sure that this is a limitation of using Babel with Typescript. What Babel does, is it simply strips the Typescript typings, and then treats the code as JavaScript.Which means babel doesn't care about your tsconfig.json at all, and thus not emitDecoratorMetadata either.

So unfortunately, if you need the decorator metadata, you'll have to stick with tsc