Search code examples
typescriptreflectiontypescript-2.5class-properties

Uninitialized TypeScript class properties are not iterated


I have the following class:

export class SomeModel {
  prop1: number;
  prop2: number;
  comment: string;
}

and the following method to dynamically get its properties:

getTypeProperties<T>(obj: T): string[] {
    const ret: string[] = [];
    for (const key in obj) {
      if (obj.hasOwnProperty(key))
        ret.push(key);
    }
    return ret;
}

The following call returns an empty array:

getTypeProperties(new SomeModel());

However, if I explicitly initialize all properties with null, the properties will be returned correctly:

export class SomeModel {
  prop1: number = null;
  prop2: number = null;
  comment: string = null;
}

Question: Is this normal behavior? Or is there a TypeScript compiler switch to toggle this?

I do not know if it is relevant, but here is the tsconfig.json content:

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "dom"
    ]
  }
}

Solution

  • This is by design, field declarations do not output any JavaScript code, they just tell the compiler that the field exists (ie. it's expected don't complain when I use it in code) and is of a certain type. Until you first assign the field it will not exist on the instance and thus will not be iterated. If you initialize the field it's value will be assigned to the instance in the constructor and will thus become iterable.

    As you have discovered the simplest way around this is to assign a value to the field, if only the value undefined.

    We can see this behavior in the code generated for ES5. For example for this class

    class A {
        nonInitField: number;
        initField = 0;
        test() {
            this.nonInitField = 0;// Can be used, and will be iterable after it is assigned
        }
    }
    

    This code is generated:

    var A = /** @class */ (function () {
        function A() {
            this.initField = 0; // Iterable right away as it is assigned in the constructor
        }
        A.prototype.test = function () {
            this.nonInitField = 0; // Can be used, and will be iterable after it is assigned
        };
        return A;
    }());