Jane Smith
Jane Smith

Reputation: 21

new object will be empty

I can't get this code to work.

import "reflect-metadata";

export class Castable {
  [key: string]: any;
  constructor(source: any) {
    console.log("source: ");
    console.log(source);
    Object.getOwnPropertyNames(source).forEach((propertyKey) => {
      const designType = Reflect.getMetadata("design:type", this, propertyKey);
      const customType = Reflect.getMetadata("custom:type", this, propertyKey);
      const type = customType !== undefined ? customType : designType;
      this[propertyKey] = this.convert(
        source[propertyKey],
        propertyKey,
        type,
        0
      );
    });
    console.log("after constructor this: ");
    console.log(this);
  }

  private convert(
    source: any,
    propertyKey: string,
    type: any,
    depth: number
  ): any {
    if (type === undefined) {
      return source;
    }
    switch (type.name) {
      case "Number":
        return Number(source);
      case "String":
        return String(source);
      case "Boolean":
        return source.toString() === "true";
      default:
        return new type(source);
    }
  }
}

/**  --- TreeRoot ---  */
export class MyConfigRoot extends Castable {
  result: boolean;
  count: number;
}

function init() {
  const json = '{"result":true, "count":32}';
  let input = JSON.parse(json);
  let newR = new MyConfigRoot(input);
  console.log("after new: ");
  console.log(newR);
}

init();

After getting the type with Reflect.getMetadata, type checking is performed. This code would result in an empty new object.

> node ./dist/test.js

source:
{ result: true, count: 32 }
after constructor this:
MyConfigRoot { result: true, count: 32 }
after new:
MyConfigRoot { result: undefined, count: undefined }

The assignment in constractor seems to succeed, but actually comes up empty. It is actually deeper than that, but it is a minimal structure to isolate the problem. Why would it be empty?

Upvotes: 1

Views: 72

Answers (1)

hackape
hackape

Reputation: 19957

This is caused by the useDefineForClassFields compilerOptions, or the lack of it. Because if not manually set, its default value is correlated to target option. From docs:

This flag is used as part of migrating to the upcoming standard version of class fields. TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the upcoming specification has a different runtime behavior to TypeScript’s implementation but the same syntax.

This flag switches to the upcoming ECMA runtime behavior.

Default: true if target is ES2022 or higher, including ESNext, false otherwise.

Feel free to read the detailed backstory. Short explainer to your case goes like this:

/**  --- TreeRoot ---  */
class MyConfigRoot extends Castable {
  result: boolean; // 👈
  count: number;
}

These type-only annotations in TS used to be harmless and have no JS runtime effect. However when useDefineForClassFields is introduced, the compilation result changes.

  • useDefineForClassFields: true
/**  --- TreeRoot ---  */
class MyConfigRoot extends Castable {
    constructor() {
        super(...arguments);
        Object.defineProperty(this, "result", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
        Object.defineProperty(this, "count", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
    }
}
  • useDefineForClassFields: false
/**  --- TreeRoot ---  */
class MyConfigRoot extends Castable {
}

Thus the behavior you observed.


Besides tuning TS compiler option, you can also use the declare keyword in your TS v3.7+ code to fine control the JS compilations.

/**  --- TreeRoot ---  */
class MyConfigRoot extends Castable {
  declare result: boolean; // 👈 declare keyword
  count: number;
}

// Now `useDefineForClassFields: true` COMPILES TO:

/**  --- TreeRoot ---  */
class MyConfigRoot extends Castable {
    constructor() {
        super(...arguments);
        Object.defineProperty(this, "count", {
            enumerable: true,
            configurable: true,
            writable: true,
            value: void 0
        });
    }
}

Upvotes: 1

Related Questions