I have an abstract Typescript class with a constructor. This constructor accepts an object and then tries to set properties of the class with the data in the object.
export default class AbstractModel {
constructor(properties: object) {
Object.assign(this, properties);
}
}
Then there are several classes inheriting this AbstractModel
class.
For example:
import AbstractModel from "./abstract-model";
export default class Derived extends AbstractModel {
firstname!: string;
lastname!: string;
age: number = 1;
}
My assumption was that when I create a new Derived
and pass {age: 10}
in the constructor the newly created Derived
would have age
set to 10
. However, age always seems to be the initial value of 1
. Other properties without an initial values are being set as expected.
const derived = new Derived({firstname : "John", lastname: "Doe", age: 10});
Output:
Derived {
firstname: 'John',
lastname: 'Doe',
age: 1,
}
Logging this in the constructor of AbstractModel
gives me the following output
export default class AbstractModel {
constructor(properties: object) {
Object.assign(this, properties);
console.log(this);
}
}
Output:
Derived {
firstname: 'John',
lastname: 'Doe',
age: 10,
}
Logging this in the constructor of Derived
gives me the following output
export default class Derived extends AbstractModel {
firstname!: string;
lastname!: string;
age: number = 1;
constructor(properties: object) {
super(properties);
console.log(this);
}
}
Output:
Derived {
firstname: 'John',
lastname: 'Doe',
age: 1,
}
Can anyone tell me why it is behaving like this?
The initial value you provide for age
is assigned/defined¹ by your subclass constructor. That assignment/definition is done just after the call to super
in your subclass constructor (either your explicit constructor if you provide one, or the default generated one if you don't). As a result, age
will be 1
in your subclass, because the assignment done by the superclass constructor gets overwritten. Here's a simpler example (just using JavaScript so we can run it in Stack Snippets):
class Base {
constructor() {
this.age = 42;
}
}
class Derived extends Base {
age = 1;
}
console.log(new Derived().age);
The generated constructor for Derived
is:
constructor(...args) {
super(...args);
}
..and the definition/assignment¹ setting age
to 1
happens just after the super(...args)
part of that.
I think the minimal-changes way to fix it in the code you've shown is to remove the default value and provide a constructor for Derived
:
age: number;
constructor(obj: object) {
super(obj);
this.age = (obj as any).age ?? 1;
}
or
age!: number;
// ^−−− Note the "definitely assigned" assertion
constructor(obj: object) {
super({age: 1, ...obj});
}
or similar (playground link).
That said, the AbstractModel
constructor code isn't typesafe, it'll copy over any and all own properties of the object you provide it. You may want to refactor that.
¹ Whether Derived
does an assignment (effectively, this.age = 1
) or a redefinition (Object.defineProperty(this, "age", {value: 1, /*...*/})
) depends on the useDefineForClassFields
configuration option. It does assignment when the option isn't enabled, and redefinition when it is. Redefinition is the way JavaScript's class fields are standardized, so the configuration option was added to TypeScript to support that newly-standard behavior when class fields were introduced to JavaScript.