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

Try it: interfaces and classes

Interview questions