I am using Typescript with Playwright and I am not certain what would be a best practice for Page object models.
For example:
export class OrderConfirmationPage extends GenericPage {
readonly orderConfirmationMessage: Locator;
constructor(page: Page) {
super(page);
this.orderConfirmationMessage = page.locator(
'.checkout-success__body__headline'
);
}
public getOrderConfirmationMessage(): Locator {
return this.orderConfirmationMessage;
}
}
Is this getter method necessary or is it the same if I access the field directly?
Directly:
orderConfirmationPage.orderConfirmationMessage
With getter:
orderConfirmationPage.getOrderConfirmationMessage()
CodePudding user response:
The differences have to do with where, when, and to what extent the "read-onliness" is enforced.
A readonly
property in TypeScript is a purely compile-time construct; the readonly
part gets erased from the emitted JavaScript along with the rest of the static type system. So while you would be warned at compile time about writing to a readonly
property, nothing stops it from happening at runtime:
class Foo { readonly a: number = 1; }
const foo = new Foo();
foo.a = 2; // compiler error, but no runtime error
// Cannot assign to 'a' because it is a read-only property.
console.log(foo.a) // 2, not 1
Of course, the same could be said for almost all of TypeScript's static type system. Nothing "stops" you from doing this:
const thisToo: string = 1; // compiler error
But we generally assume that TypeScript developers will fix such errors before they make it to runtime, or that any runtime violations of the TypeScript types are out of scope.
Arguably worse is the fact that readonly
properties only affect direct writes and not type compatibility. So the following code is completely legal TypeScript:
interface Bar {a: number}
const bar: Bar = foo; // okay
bar.a = 3; // okay
console.log(foo.a) // 3
There is a longstanding open request at microsoft/TypeScript#13347 to prevent compatibility between readonly
and mutable properties, but for now it's not part of the language.
On the other hand, you can't assign to the return value of method, it's a syntax error to try:
class Foo {
readonly a: number = 1;
getA() { return this.a }
}
// foo.getA() = 4; // compiler error and runtime error
So a method will definitely make its output effectively readonly, even at runtime. Of course, having both a readonly
property and a method defeats the purpose. The method is like a locked door, but the readonly
property it references is like an open door with a sign on it that says "don't use this door". Whether or not that is acceptable depends on how badly you need to guard access to whatever's behind the door.
If you really want to enforce read-onliness from the outside, you could do several things. One is that you could use the Object.defineProperty()
method to make the property non-writable
even at runtime:
class Foo {
readonly a!: number;
constructor() {
Object.defineProperty(this, "a", { value: 1 })
}
}
const foo = new Foo();
try {
foo.a = 2; // compiler error,
} catch (e) {
console.log(e); // "a" is read-only
}
Or you could use a private property along with an actual getter accessor, which is conceptually similar to what you were doing:
class Foo {
#a: number = 1;
get a() {
return this.#a
}
}
const foo = new Foo();
try {
foo.a = 2; // compiler error,
} catch (e) {
console.log(e); // setting getter-only property "a"
}
Note: none of this has any bearing on whether or not the property is itself a mutable object. A primitive like a number
can't be modified, but an object can, and neither readonly
, nor private, nor getter, will do anything about that:
class Baz {
#p: { a: number } = { a: 1 }
getP() { return this.#p };
}
const baz = new Baz();
// baz.#p = {a: 5} // invalid at runtime
// baz.getP() = {a: 5} // invalid at runtime
baz.getP().a = 5; // no error at compile time or runtime
console.log(baz.getP().a) // 5
There are ways to avoid that, but a discussion of it would digress from the subject of the question as asked.