网站搜索

playwright 测试 - TypeScript 只读属性和 getter 方法


我正在使用 TypeScript 和 Playwright,我不确定页面对象模型的最佳实践是什么。
例如:

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;
  }
}  

这个 getter 方法有必要吗,还是直接访问字段效果一样?
直接访问:

orderConfirmationPage.orderConfirmationMessage

带有 getter:

orderConfirmationPage.getOrderConfirmationMessage()  

答案#1

这些差异与“只读性”在哪些地方、何时以及在何种程度上被执行有关。


在 TypeScript 中,readonly 属性是一个纯粹的编译时结构;readonly 部分会与其他静态类型系统一起从生成的 JavaScript 中被擦除。因此,虽然在编译时会对写入 readonly 属性发出警告,但在运行时没有什么能阻止它发生:

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

当然,几乎可以对 TypeScript 的整个静态类型系统说同样的话。没有什么能“阻止”你这样做:

const thisToo: string = 1; // compiler error

但我们通常假设 TypeScript 开发者会在错误进入运行时之前修复它们,或者任何对 TypeScript 类型的运行时违反都是不在范畴内的。

可以说更糟糕的是,readonly 属性只影响直接写入,而不影响类型兼容性。因此,以下代码在 TypeScript 中是完全合法的:

interface Bar {a: number}
const bar: Bar = foo; // okay
bar.a = 3; // okay
console.log(foo.a) // 3

在 microsoft/TypeScript#13347 上长期存在一个公开请求,旨在防止 readonly 属性与可变属性之间的兼容性,但目前它还不是语言的一部分。


另一方面,你不能给方法的返回值赋值,尝试这样做会导致语法错误:

class Foo {
    readonly a: number = 1;
    getA() { return this.a }
}

// foo.getA() = 4; // compiler error and runtime error

因此,一个方法将肯定使其输出在运行时也有效地为只读。当然,同时拥有一个 readonly 属性和一个方法是违背初衷的。这个方法就像一扇上锁的门,而它引用的 readonly 属性就像一扇贴着“不要使用此门”标志的敞开门。是否可以接受这种情况取决于你多么需要保护门后面的内容的访问权限。


如果你真的想从外部强制只读,你可以做几件事。一个方法是使用 Object.defineProperty() 方法,使属性即使在运行时也不可 writable:

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 
}

或者你可以使用私有属性以及实际的 getter 访问器,这在概念上类似于你正在做的事情:

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" 
}

注意:这些都与属性本身是否是可变对象无关。像 number 这样的原始类型不能被修改,但对象可以,并且 readonly、private 或 getter 都无法对此产生任何影响:

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

有办法可以避免那种情况,但讨论它会偏离所提问题的主题。

代码的 Playground 链接