QA Interview Hub · Knowledge base · Quizzes
TypeScript
Classes and access modifiers
Access modifiers and parameter properties
TS has three access modifiers:
public is the default, accessible from anywhere.protected is accessible in the class and its subclasses.private is only inside the class.
Important: these are compiler checks. At runtime a private field is an ordinary object property. Real privacy is given by JS private fields with #: #token is inaccessible from outside even at runtime.
export class LoginPage {
private readonly emailInput: Locator;
readonly submitButton: Locator;
constructor(private readonly page: Page) {
this.emailInput = page.getByLabel('Email');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.page.getByLabel('Password').fill(password);
await this.submitButton.click();
}
}
constructor(private readonly page: Page) is a parameter property: it declares the field and assigns it at the same time. It is convenient, but since TS 5.8 there is the option erasableSyntaxOnly, which forbids this syntax together with enum, because it generates JS code rather than simply being erased. It is relevant if TS is run directly through type stripping in Node. In an ordinary Playwright project parameter properties work.
readonly on a field means it can be assigned only at declaration or in the constructor.
How I decide what to make public in a Page Object: public are business-level actions (login, makeTransfer) and locators needed for expect in tests. Internal details are private.
In an interview: public, protected and private are checked only by the compiler, and for real privacy at runtime there are fields with #. In a Page Object I make business actions and locators for checks public, and hide page and helper methods.
abstract, implements and inheritance in a Page Object
An abstract class can't be created directly. It sets shared logic and forces subclasses to implement abstract methods.
export abstract class BasePage {
protected abstract readonly path: string;
constructor(protected readonly page: Page) {}
async open(): Promise<void> {
await this.page.goto(this.path);
await this.waitForLoaded();
}
protected abstract waitForLoaded(): Promise<void>;
}
export class AccountsPage extends BasePage {
protected readonly path = '/accounts';
protected async waitForLoaded(): Promise<void> {
await expect(this.page.getByRole('heading', { name: 'Accounts' })).toBeVisible();
}
}
implements checks that a class conforms to an interface, but inherits nothing. This is useful for interchangeable implementations: for example PaymentsApi and PaymentsMock implement one interface.
interface PaymentsGateway {
pay(amount: number, currency: string): Promise<{ txId: string }>;
}
class PaymentsApi implements PaymentsGateway {
async pay(amount: number, currency: string) {
return { txId: 'real-id' };
}
}
An honest position on inheritance: deep Page Object hierarchies (BasePage, then AuthorizedPage, then DashboardPage) quickly become brittle. I prefer composition: a page consists of components (Header, TransferForm, Table), and shared things are passed through fixtures. One level of a base class is fine, three is already a signal to refactor.
static members belong to the class, not the instance. They suit factories or constants, but not state: static state is shared between tests in a worker and can cause flakiness.
In an interview: An abstract class gives shared logic and forces subclasses to implement the needed methods, while implements only checks the contract. In a Page Object I keep at most one level of inheritance and prefer composition from components.
The this trap and arrow methods
In JS the value of this is determined by how a function is called, not where it is declared. If you pass a class method as a callback, this is lost.
class TokenStore {
private token = '';
set(value: string) { this.token = value; }
get() { return this.token; }
}
const store = new TokenStore();
const getter = store.get;
// getter(); // TypeError at runtime: this === undefined
Classes in JS are always in strict mode, so this will be undefined, not the global object.
Solutions:
- Pass an arrow:
items.map(x => helper.format(x)). - Bind:
store.get.bind(store). - Declare the method as an arrow property:
get = () => this.token;. Such a function captures the instance's this. The downside is that it is created for each instance and doesn't live on the prototype.
TS helps partly: you can explicitly declare a this parameter in the signature (function f(this: Foo)), and then a wrong call won't compile. But for ordinary class methods the compiler doesn't catch the loss of this when passed as a callback.
Where I saw this in automation: passing a Page Object method into test.step or into a retry helper as retry(page.reload). I write retry(() => page.reload()), and there is no problem.
In an interview: this in JS depends on how a function was called, so a method passed as a callback loses its context. I pass an arrow wrapper function or use bind, and I pay attention to this in code review.