5

I have the following class with a constructor that defines properties on it. I need the constructor to accept an object which only contains the (extending) class members

abstract class A {
    constructor(initializer: Partial<this>) { // <-- using this type in a constructor is not allowed!
        for (const [key,value] of Object.entries(initializer)) {
            Object.defineProperty(this, key, { value, enumerable: true })
        }
    }
    static someStaticMethod() {}
    someInstancemethod() {}
    
}

this class is only used to be extended by other classes

class B extends A {
    a?: string
    b?: number
    c?: boolean
}

new B({ a: "hello", b: 1, c: true, d: "this must error" }) // <-- I want only properties of B here in the constructor argument

I know I can add a type argument to A and then assign the extending class to it

abstract class A<T> {
    constructor(initializer: Partial<T>) {
        ...
    }
}

class B extends A<B> {
...
}

But this solution of repeating B doesn't look elegant, especially considering that this is an external API for a library I'm working on. Is it possible to achieve what I want, leaving the syntax as in the first example: class B extends A? If not, what is a possible workaround? Playground Link

7
  • 1
    Repeating B as a generic argument is a pretty standard pattern called the curiously recurring template pattern. I'll agree that it's not the most elegant solution, but it is pretty standard. Commented Sep 3, 2022 at 18:16
  • No, this is not possible. There is a request at ms/TS#38038 to support something like this, and if you want you can go there and give it a 👍 and describe your use case, but it's not part of the language now and might not be ever. Are you interested in other workarounds (if so you should edit the question to specify this)? Or do you just want an answer that says "no" with documentation? Let me know (and mention me with @jcalz if you reply so I am notified) Commented Sep 3, 2022 at 18:17
  • 1
    ...hmm, how do you plan private to work in your subclasses? Partial<B> has no access to the a, b, or c properties. When you write "I know I can add a type argument to A and then assign the extending class to it", have you tried that? It's impossible to pass in a, b, or c there. Could you fix this in your question so there's only one stumbling block and not multiple? Commented Sep 3, 2022 at 18:22
  • Thanks. Does this workaround work for you? You can emulate this in static methods, but not in the constructor itself. If this addresses your question I can write up an answer explaining it with relevant documentation. If not, what am I missing? Commented Sep 3, 2022 at 18:52
  • thank you @jcalz, this answers my question. I would appreciate it if you could include some hint about why this isn't supported by ts in your answer Commented Sep 3, 2022 at 19:03

1 Answer 1

3

It is not currently possible in TypeScript to use this types in the call signature for a class constructor() method. There is an open feature request at microsoft/TypeScript#38038 asking for such support, but for now it's not part of the language.

(A comment asked why it isn't already supported, but I don't have an authoritative answer for that. I can guess: The constructor method is unique in that it is a static method whose this context is that of a class instance, so presumably implementing this typing for its call signature would have required some conscious effort to do. And it's not a feature the community is clamoring for, as evidenced by the relatively few upvotes on the issue linked above. Those are probably contributing factors for why it is not already supported. But, as I said, these are guesses.)


Until and unless that is implemented you'd need to use workarounds.

One such workaround would be to use a static method with this types instead of the constructor, so you call B.make({}) instead of new B({}).

Unfortunately this types are also not supported for static methods. Support is requested at microsoft/TypeScript#5863. But you can simulate such behavior with this parameters and a generic type parameter, as mentioned in this comment on that GitHub issue:

abstract class A {
    static make<T extends A>(
      this: new (initializer: Partial<A>) => T, 
      initializer: Partial<T>
    ) {
        return new this(initializer);
    }
    constructor(initializer: Partial<A>) {
        for (const [key, value] of Object.entries(initializer)) {
            Object.defineProperty(this, key, { value, enumerable: true })
        }
    }
    static someStaticMethod() { }
    someInstancemethod() { }

}

The make() method can only be called on a concrete constructor which accepts a Partial<A> and produces an instance of type T constrained to A. So you can't call A.make() directly for the same reason you can't call new A(), since the constructor is abstract:

A.make({}) // error!
// <-- Cannot assign an abstract constructor type to a non-abstract constructor type

But you can subclass as desired, and the static make() method will be inherited:

class B extends A {
    a?: string
    b?: number
    c?: boolean
}

When you call B.make(), then, the compiler infers that T is B and so the parameter to make() is Partial<B>, which, among other things, will reject excess properties:

const b = B.make({ a: "hello", b: 1, c: true }); // okay
// const b: B
console.log(b); // {a: "hello", b: 1, c: true}

B.make({ a: "hello", b: 1, c: true, d: "this must error" }); // error!
// -------------------------------> ~~~~~~~~~~~~~~~~~~~~
// Object literal may only specify known properties

Playground link to code

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

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.