Reputation: 5850
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:
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 }
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).
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.
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
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. 😅
Upvotes: 2