2

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()  
mismas
  • 1,236
  • 5
  • 27
  • 55
  • 1
    @jcalz is this better now? – mismas Oct 27 '22 at 18:03
  • You get the same type safety with both approaches, so personally I would not bother with the getter. – Linda Paiste Oct 27 '22 at 18:14
  • It's not exactly "the same". The TS compiler will warn you if you try to set a property that's marked as `readonly` but there is no runtime enforcement. Whereas the `getXXX()` method cannot be used to set the property (but of course the actual property is still exposed at runtime, so it's like having a locked door next to an open door). – jcalz Oct 27 '22 at 18:17
  • Note that you can get runtime enforcement of the behavior by making an [actually private property](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields) and an [actual getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get), like [this](https://tsplay.dev/wRXQ1m). Does that fully address your question? If so I could write up an answer; if not, what am I missing? (If you reply pls mention @jcalz to notify me) – jcalz Oct 27 '22 at 18:17
  • @jcalz Thanks, please post an answer. This answers completely my question – mismas Oct 27 '22 at 18:41

1 Answers1

2

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.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360