level 01 / interfaces-and-classes
Interfaces & Classes
TypeScript interfaces, type aliases, and classes — the building blocks of Playwright page objects, fixtures, and typed test data.
Interfaces
An interface describes the shape of an object:
interface User {
id: number;
username: string;
email: string;
role: 'admin' | 'customer';
}
// Optional properties
interface TestOptions {
timeout?: number; // may be undefined
retries?: number;
baseURL?: string;
}
// Readonly properties
interface Config {
readonly baseURL: string; // cannot be reassigned after creation
timeout: number;
}
Type aliases vs interfaces
// Type alias — can represent any type, including primitives and unions
type Status = 'pass' | 'fail' | 'skip';
type ID = string | number;
type Callback = () => void;
// Interface — describes object shapes; supports declaration merging
interface PageObject {
goto(): Promise<void>;
isLoaded(): Promise<boolean>;
}
// When to use which:
// - Object shapes in page objects → interface (extensible, familiar)
// - Union types, primitives, tuples → type alias (interfaces can't)
// - Either works for simple object types — pick one and be consistent
Key insight: Playwright’s own types use interfaces throughout. Follow the same pattern in your page objects for consistency with the library you’re using.
Classes
class LoginPage {
// Constructor shorthand — declares and assigns properties in one go
constructor(private readonly page: Page) {}
async goto(): Promise<void> {
await this.page.goto('https://www.saucedemo.com');
}
async signIn(username: string, password: string): Promise<void> {
await this.page.getByPlaceholder('Username').fill(username);
await this.page.getByPlaceholder('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
async getError(): Promise<string> {
return this.page.getByTestId('error').innerText();
}
}
// Usage in a test
test('login succeeds', async ({ page }) => {
const login = new LoginPage(page);
await login.goto();
await login.signIn('standard_user', 'secret_sauce');
await expect(page.getByText('Products')).toBeVisible();
});
Access modifiers
class BasePage {
protected page: Page; // accessible in this class + subclasses
private _isLoaded = false; // accessible only in this class
public url: string; // accessible anywhere (default)
readonly baseURL: string; // assignable only in constructor
constructor(page: Page, baseURL: string) {
this.page = page;
this.url = baseURL;
this.baseURL = baseURL;
}
protected async waitForLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
this._isLoaded = true;
}
}
class InventoryPage extends BasePage {
async getItemCount(): Promise<number> {
await this.waitForLoad(); // OK — protected method in base class
return this.page.locator('.inventory_item').count();
}
}
Interfaces + classes together
// Interface defines the contract
interface ILoginPage {
goto(): Promise<void>;
signIn(username: string, password: string): Promise<void>;
getError(): Promise<string | null>;
}
// Class implements the contract
class LoginPage implements ILoginPage {
constructor(private page: Page) {}
async goto(): Promise<void> {
await this.page.goto('https://www.saucedemo.com');
}
async signIn(username: string, password: string): Promise<void> {
await this.page.getByPlaceholder('Username').fill(username);
await this.page.getByPlaceholder('Password').fill(password);
await this.page.getByRole('button', { name: 'Login' }).click();
}
async getError(): Promise<string | null> {
const el = this.page.getByTestId('error');
if (await el.isHidden()) return null;
return el.innerText();
}
}