4

The example below is a raw representation of what’s going on in real code (I'm not working with HTML or front-end. The code below is a dummy code. Sorry if it does not make any sense from front-end perspective)

TL, DR

I’m struggling to specify a return type of method which will return an array of class instances of the class from which this method was called.

Back to dummy code:

PageElement.ts

class PageElement {
    public element: HTMLElement
    constructor(
        public elementSelector: string,
        public elementName: string,
        public description: string,
        
    ) {
        this.element = document.getElementById(this.elementSelector)!;
        
    }
 
    getAll() {
        let foundElements = document.querySelectorAll(this.elementSelector);
        let returnElements = [];
        let constructor = this.constructor as any;
        foundElements.forEach(
            element => returnElements.push(
                new constructor(
                    element.attributes['id'].value,
                    this.elementName + (returnElements.length + 1),
                    this.description
                )
            )
        )
 
        return returnElements;
    }
}

Let’s say this class has lots of child classes, e.g.:

class InputElement extends PageElement {
    constructor(
        elementSelector: string, 
        elementName: string, 
        description: string) {
        super(elementSelector, 
            elementName, 
            description
        )
    }
 
    exclusiveInputMethod() {
        // this method exist only on InputElement 
    }
}

There are two core issues I would like to fix

getAll() {
        let constructor = this.constructor as any;
}

The logic above works fine in JS like new this.constructor(...) . However, TS does not allow to do this with error: This expression is not constructable. Type 'Function' has no construct signatures.ts(2351)

So I need to do let constructor = this.constructor as any;. But this kills IntelliSense and all type safety.

Of course, you could say: “Just return an array of PageElement, duh”:

getAll() {
        let foundElements = document.querySelectorAll(this.elementSelector);
        let returnElements = [];
        foundElements.forEach(
            element => returnElements.push(
                new PageElement(
                    element.attributes['id'].value,
                    this.elementName + (returnElements.length + 1),
                    this.description
                )
            )
        )
 
        return returnElements;
    }

But here is where the problem with inheritance comes up:

class InputElement extends PageElement {
    constructor(
        elementSelector: string, 
        elementName: string, 
        description: string) {
        super(elementSelector, 
            elementName, 
            description
        )
    }
 
    exclusiveInputMethod() {
        // do stuff
    }
}

In case getAll will return an array of PageElement I will not be able to access the exclusiveInputMethod. Because when I’ll call InputElement.findAll() it will return PageElement[] instead of InputElement[].

I’ll summarize my questions:

What is the proper equivalent of new this.constructor for TS so it knows, that I’m creating an instance of this particular class?

The second question is (well, it’s actually the same as first): How can I annotate (or make TS to infer return type properly) method getAll() so it always knows that the return type is an array of the class instance from which this method was called?

2 Answers 2

4
+50

Specify this[] as the return type of your method:

class PageElement {
  getAll(): this[]  {}
}

class InputElement extends PageElement {
  aSpecificInputElementMethod() {}
}

const inputElement = new InputElement();
const inputElements = inputElement.getAll();

inputElements[0].aSpecificInputElementMethod(); // valid;

See it in action on TS Playground.

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

Comments

2

Not actually an ideal way, but you can specify the return type as the type argument

getAll<T>(): T[]

And then call like

getAll<InputElement>

The downsides are:

  • You need to specify type argument every time you call getAll
  • Type argument should always be a valid type
  • You can pass any type of an argument

So basically there is not a lot of type safety because it’s up to you to keep the type argument valid every time you call getAll. This will probably cause issues if you are working in a team.

Maybe someone can suggest a way on how to restrict generic type to be an instance of this particular type.

1 Comment

Yeah... this approach has some potential human-related issues. I don't know the way how to validate what caller is passing in <...>. But thanks anyway

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.