0

I'm attempting to create a class hierarchy with a recursive array that references it self, but also correctly assigns type to subclasses. I think I'm close, but I'm getting an inexplicable TS2351 error

export interface ContentNodeJSON {
  id: string
  parentId?: string
  children: ContentNodeJSON[]
}

export class ContentNode {
  id: string
  parentId?: string
  children: this[]

  constructor(model: ContentNodeJSON) {
    this.id = model.id;
    this.parentId = model.parentId;
    this.children = model.children.map(child => new this(child));
  }
}

The error is as follows:

This expression is not constructable.
  Type 'ContentNode' has no construct signatures.  TS2351
    20 |     this.parentId = model.parentId;
  > 21 |     this.children = model.children.map(child => new this(child));
       |                                                     ^
    22 |   }

Is it just impossible to call a constructor from itself? Is there some other pattern I should be using to accomplish this goal?

Edit:

To clarify what I mean by "correctly assigns type to subclasses", if I define a subclass as:

class Page extends ContentNode {}

Then Page.children needs to be of type Page, not ContentNode.

2
  • You have a typo as well: id: string ( Commented Dec 5, 2019 at 20:28
  • children: this[] should be children: ContentNode[] Commented Dec 5, 2019 at 20:29

2 Answers 2

1

You want to call a constructor, so it should be this.constructor instead of just this. However, Typescript doesn't seem to like that, since this.constructor is of type Function which isn't a constructor signature; I don't know why Typescript doesn't know that a constructor is a constructor, but we can make a type assertion to say that we know it is:

    const _constructor = this.constructor as new (...args: any[]) => this;
    this.children = model.children.map(child => new _constructor(child));

Note that there's something a bit fishy about children: this[], since if obj has the type ContentNode then you can write obj.children.push(new ContentNode(...)) and it will type-check even though obj could be a Page instance at runtime. This issue could be avoided by declaring it as readonly children: ReadonlyArray<this>.

You're also constraining subclasses to have a constructor taking a single parameter of type ContentNodeJSON, but beware that this constraint won't be checked at compile-time.

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

2 Comments

I’m new to typescript, so this pattern may not be idiomatic. It’s very common in Swift where you will type a collection as [Self] and subclasses will all automatically infer the correct type at runtime. I can see how it may be less valuable when all our type safety disappears at runtime.
I've edited to mention that you can declare readonly children: ReadonlyArray<this> as a solution to that problem. That doesn't make the array actually immutable, but the compiler will check that you aren't mutating it.
0

This works

export interface ContentNodeJSON {
    id: string;
    parentId?: string;
    children: ContentNodeJSON[];
}

export class ContentNode {
    id: string;
    parentId?: string;
    children: ContentNode[];

    constructor(model: ContentNodeJSON) {
        this.id = model.id;
        this.parentId = model.parentId;
        this.children = model.children.map(child => new ContentNode(child));
    }
}

You can use new ContentNode inside ContentNode.constructor because under the hood it will compile into a function (or more precisely, something one could view as a function, but I digress), so it's basically recursion, and JS allows that.

To answer your edit (that's borderline a chameleon question, but anyway), you could use generic types like this:

export class ContentNode<T> {
    id: string;
    parentId?: string;
    children: ContentNode<T>[];

    constructor(model: ContentNodeJSON) {
        this.id = model.id;
        this.parentId = model.parentId;
        this.children = model.children.map(child => new ContentNode(child));
    }
}

2 Comments

See my comment in the edit, making children a ContentNode won't work.
I don't think it's a chameleon question; I understood this requirement from the original wording and the use of the polymorphic this type which I assumed was intentional.

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.