TypeStrong/ts-node

Inconsistent reflected enum types with --transpile-only enabled / disabled

LeoBakerHytch opened this issue · 1 comments

Problem

When running ts-node --transpile-only, a class field declared with an imported (string) enum gets annotated as an Object, rather than the expected String.

This causes a problem with typegoose, which uses decorators to annotate a MongoDB / Mongoose database schema. Its prop decorator runs some validations to ensure consistency between the specified field type and the constraints provided in the decorator options.

Specifically, it checks that a field decorated with @prop({ enum: MyStringEnum }) is actually a string field. With --transpile-only enabled, this check always fails (this validation was added fairly recently: szokodiakos/typegoose@7418c34).

Repro

Can be cloned from https://gist.github.com/LeoBakerHytch/a45a6ed03b27807def5ec799d5a56c26

index.ts:

import 'reflect-metadata';
import { ExternalEnum } from './enum-metadata';

enum LocalEnum {
    bar = 'bar'
}

class Class {
    @prop()
    local: LocalEnum; // OK

    @prop()
    external: ExternalEnum; // Errors only with --transpile-only: Type === Object
}

function prop() {
    return function (target, key) {
        const Type = Reflect.getMetadata('design:type', target, key);
        if (Type !== String) {
            throw Error(`Expected String for field "${key}"; got ${Type.name}`);
        }
    };
}

enum-metadata.ts:

export enum ExternalEnum {
    foo = 'foo'
}

package.json scripts:

  "scripts": {
    "ok": "ts-node index.ts",
    "ok2": "ts-node --transpile-only --type-check index.ts",
    "error": "ts-node --transpile-only index.ts"
  },

Compiled output

For comparison, TypeScript does emit the correct output when transpiling with isolatedModules: true:

    tslib_1.__decorate([
        prop(),
        tslib_1.__metadata("design:type", String) // <—— what we want
    ], Class.prototype, "external");

index.js:

"use strict";
exports.__esModule = true;
var tslib_1 = require("tslib");
require("reflect-metadata");
var enum_metadata_1 = require("./enum-metadata");
var LocalEnum;
(function (LocalEnum) {
    LocalEnum["bar"] = "bar";
})(LocalEnum || (LocalEnum = {}));
var Class = /** @class */ (function () {
    function Class() {
    }
    tslib_1.__decorate([
        prop(),
        tslib_1.__metadata("design:type", String)
    ], Class.prototype, "local");
    tslib_1.__decorate([
        prop(),
        tslib_1.__metadata("design:type", String)
    ], Class.prototype, "external");
    return Class;
}());
function prop() {
    return function (target, key) {
        var Type = Reflect.getMetadata('design:type', target, key);
        if (Type !== String) {
            throw Error("Expected String for field \"" + key + "\"; got " + Type.name);
        }
    };
}

Solution?

Is there any way around this other than enabling --type-check or disabling --transpile-only? (I’m not actually clear on whether those two flags are complementary — does it make any sense to use both flags?)

We have been using --transpile-only as restarts of our server are incredibly slow without it... it would be a shame to have to disable it.

Related

The relavant Typegoose issue is szokodiakos/typegoose#196.

I’m not sure where the best place is to solve this issue. The additional validation added in Typegoose to ensure consistency of field types & constraints makes sense, so I wouldn’t really like to suggest that it be weakened for the sake of an environment-specific problem.

You would only need one of either --type-check or --transpile-only (--type-check is there for backward compatibly and not documented anywhere as far as I know).

Unfortunately this is just how the TypeScript compiler works. For certain use-cases (such as these enums), TypeScript requires type information to compile correctly. If it needs type information, --transpile-only will be incapable of generating the expected output.