Sunyatasattva
Sunyatasattva

Reputation: 5850

Extending object member of subclass

I'm building Page Objects for my automated tests, and I'm doing something like this:

abstract class Page  {
  selectors: {
    // Here are selectors which are common to all my pages
    genericSelector: "...",
    commonContainer: {
      aButton: "..."
    }
  }

  // methods that are common to all my pages
}
class MyPage extends Page {
  
  // Here I want my page specific selectors
  // *plus* my generic selectors 
  selectors: {
    ...this.selectors,
    mySpecificSelector: "..."
  }
}

The above code works fine at run-time: I can correctly use all my selectors in my code. However, Typescript complains on MyPage.ts that:

Property 'selectors' in type 'MyPage' is not assignable to the same property in base type 'Page.

Basically because TS doesn't know that I'm spreading the base class prop into the subclass.

Now, the following is what I tried:

One additional note

My type for the selector property looks like this:

type Selector = string | SelectorFactory | { selector: string, type: "css" | "xpath" }
type SelectorFactory = (...args: any[]) => string;
type SelectorTree = { [k: string]: SelectorTree | Selector }

Convenient TS Playground link

What I tried

First attempt

class MyPage extends Page {
  selectors: {
    ...this.selectors as InstanceType<typeof Page>["selectors"]
  }
}

This, then complains:

'selectors' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.

So I have to do:

  selectors: SelectorTree = // ...

Then I'm back to square one, as it complains that it is missing some properties from the base class. If I add : SelectorTree to my base class, it then doesn't complain, but I don't get proper intellisense and I even get errors (for example if I try to call a function from my selectors properties, I can't).

Second attempt

I could, I guess, have my selectors member of the base Page class be static. Then I could do:

  selectors: {
    ...Page.selectors,
    mySpecificSelector: "..."
  }

This works, but, well, I now actually have an extra static member that I did not want to have.

Third attempt, aka “I give up”

I could separate the members and have something like baseSelectors on my Page class and then I could either use them as they are within the sub-class, or spread them into my selectors object.

But this feels like a defeat.

Upvotes: 2

Views: 76

Answers (1)

jcalz
jcalz

Reputation: 330216

This unfortunately seems to be a design limitation in TypeScript, according to microsoft/TypeScript#33899:

Due to architectural constraints around how trees are marked as checked, this has to be treated as a circular reference despite the type assertion.

Apparently any reference to this.selectors, no matter how indirect, seems to cause a circularity error. So far, the only workaround I've found that gives you the right types without having to refactor the emitted code is to widen this to Page (note: InstanceType<typeof Page> is the same as Page) to get a non-any type for this.selectors, and then just brute-force suppressing the circularity warning with //@ts-ignore:

class SubPage extends Page {
  //@ts-ignore circularity warning
  selectors = { ...(this as Page).selectors, foo: "bar", open: () => "" }
}

You can verify that instances of SubPage behave as expected:

const p = new SubPage();
p.selectors;
// SubPage.selectors: {
//   foo: string;
//   open: () => string;
//   test: string;
// }
p.selectors.open().toUpperCase(); // okay

That works! But...

⚠ ALERT ⚠ I feel very uneasy whenever someone suggests //@ts-ignore because if the suppressed error has any other unpleasant effects on your code, they will still exist. I didn't see any such effects with the example code above, but I can't be certain they're not there. If your automobile mechanic addressed a lit "check engine" indicator light by covering the indicator light so that you can't see it anymore, you'd maybe want to start looking around to find a different mechanic, if only for a second opinion. 😅

Playground link to code

Upvotes: 2

Related Questions