If I declare a class like this:
class Dog {
a: string;
b: string;
c: string;
}
TSC will complain that a, b, and c are not initialized. However, if I do this:
interface Animal {
a: string;
b: string;
}
interface Dog extends Animal {
c: string;
}
class Dog {
constructor() {}
}
it doesn't care whether the properties are initialized or not. I expected that the compiler would warn me that the properties in the merged interface weren't initialized like the one in the first snippet. What's the logic behind why that's not the case?
CodePudding user response:
I'm interpreting the question as:
Why does merging an interface
declaration into the instance side of a class
declaration not cause the compiler to warn when the interface-declared properties are not initialized in the class, even when the --strictPropertyInitialization
compiler option is enabled?"
And the answer is:
The use case for declaration merging into a class is to patch an existing class from the outside. Generally speaking, if you augment the class inferface with a new property or method from the outside, you will also initialize the property or method from the outside. So you'd start with the original class declaration:
class Dog {
constructor() { }
}
And then externally you augment the interface and also implement the added properties:
interface Animal {
a: string;
b: string;
}
interface Dog extends Animal {
c: string;
}
// implementation
Dog.prototype.a = "defaultA";
Dog.prototype.b = "defaultB";
Dog.prototype.c = "defaultC";
If you do that, it will work as expected:
const d = new Dog();
console.log(d.b); // "defaultB"
d.b = "newB";
console.log(d.b); // "newB"
Meanwhile, the use case for the --strictPropertyInitialization
compiler option is to verify that the properties declared in the class itself are properly initialized. This is a separate use case from declaration merging; any properties you need to definitely initialize in the class body should also be declared in the class body.
So that's the answer to the question as asked. It seems you have an underlying need to create a class constructor from an interface without re-declaring the properties in the class, and your attempt to do this was with declaration merging... which is, unfortunately, not the right tool for the job (at least as of TypeScript 4.9).
There are other approaches; sometimes I use what I call an "assigning constructor" factory which only needs to be implemented once:
function AssignCtor<T extends object>(): new (init: T) => T {
return class { constructor(init: any) { Object.assign(this, init); } } as any;
and then you can use it to generate class constructors:
interface IDog {
a: string;
b: string;
c: string;
}
class Dog extends AssignCtor<IDog>() {
bark() {
console.log("I SAY " this.a " " this.b " " this.c "!")
}
};
const d = new Dog({ a: "a", b: "b", c: "c" });
d.bark(); // I SAY a b c!
This may or may not meet your needs, and it's out of scope for the question as asked in any case. My point is just that you will probably want to look somewhere other than declaration merging to scratch this particular itch.
CodePudding user response:
As jcalz has mentioned, TypeScript currently does not check for initialized properties with merged declarations. Thus currently, the only way to ensure that the properties are initialized would be to declare them directly within the class like this:
class Dog implements Animal {
a: string;
b: string;
c: string;
constructor() {
this.a = 'a'
this.b = 'b'
this.c = 'c'
}
}
However, I still don't think this is the ideal solution and I have submitted an issue about this on the TypeScript repo. You can see it here: https://github.com/microsoft/TypeScript/issues/52279