1

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?

11
  • 1
    Merging like that is intended to describe external augmentation of an existing class, not to check the class body itself... if you want that to be checked presumably you would write class Dog extends Animal (or implements Animal) and then initialize properties in the class itself. Does that fully address the question? If so I could write up an answer explaining; if not, what am I missing? Commented Jan 17, 2023 at 20:11
  • 1
    A class can't extend a type. I could do implements instead, but I didn't want to reuse the properties. Commented Jan 17, 2023 at 20:13
  • Sure, if Animal is not a superclass then you can't extend it. What do you mean by "reuse" the properties? As asked, the question wants to know why declaration merging doesn't require class property initialization, now how to work around it, so... would you accept an answer saying something like "--strictPropertyInitialization is not intended to warn about anything that might be externally merged (presumably those properties would be externally initialized also)" along with links to sources? Or am I missing something? Commented Jan 17, 2023 at 20:17
  • I guess the problem was that I was expecting the compiler to warn me that the properties in the merged interface weren't initialized like the one in the first snippet. Would --strictPropertyInitialization solve this? Commented Jan 17, 2023 at 20:20
  • Can not reproduce the first example you have. Class declaration with properties should not be a problem in ts. There is a missing part somehow to reproduce your first example error. Commented Jan 17, 2023 at 20:22

2 Answers 2

2

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.

Playground link to code

Sign up to request clarification or add additional context in comments.

Comments

0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.