TypeScript Classes for Beginners: Constructors, Inheritance, Access Modifiers and Page Objects

Classes are the heart of object-oriented programming and the reason Playwright frameworks stay clean as they grow. This beginner guide explains TypeScript classes in plain language, from your first constructor to a working Page Object Model.

Advertisement

📖 What You’ll Learn: class syntax, constructors, parameter properties, public/private/protected/readonly, inheritance with extends and super, getters and setters, static members, abstract classes, implementing interfaces, and Page Object Model classes in Playwright.


🏗️ What Is a Class?

A class is a template for creating objects. It bundles data (properties) and behavior (methods) together. Each object created from a class is called an instance.

A simple analogy: a class is the architect’s plan for a house, and each house built from that plan is an object. Every house has the same rooms (properties) and the same doors and switches (methods), but each one can have different paint and furniture (values).

In Playwright, classes are the foundation of the Page Object Model: each page of your application becomes a class with locators as properties and user actions as methods.


✍️ Creating Your First Class

class Car {
  brand: string;
  speed: number;

  constructor(brand: string, speed: number) {
    this.brand = brand;
    this.speed = speed;
  }

  describe(): string {
    return this.brand + ' runs at ' + this.speed + ' km/h';
  }
}

const car1 = new Car('Tata', 120);   // create an instance (object)
const car2 = new Car('Honda', 140);

console.log(car1.describe()); // Tata runs at 120 km/h
console.log(car2.brand);      // Honda

Key words to remember:

  • class declares the template.
  • constructor runs once when you call new and sets up the starting values.
  • this refers to the current object.
  • new creates a fresh object from the class.

⚠️ Common mistake: Forgetting this. inside a method is the most common beginner error. Write this.speed, not speed, to read the object’s own property.


⚡ Parameter Properties: The Short Way

TypeScript lets you declare and assign properties directly in the constructor by adding an access modifier (public, private, protected or readonly) before the parameter. You will see this in almost every Playwright page object.

// Long form
class UserLong {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

// Short form: same result, less code
class User {
  constructor(public name: string, private password: string) {}
}

const u = new User('admin', 'secret');
console.log(u.name);       // admin
// console.log(u.password); // ❌ Error: 'password' is private

🔐 Access Modifiers: public, private, protected, readonly

Access modifiers control who can use a property or method.

ModifierAccessible fromTypical use
public (default)AnywhereMethods tests call, like login()
privateOnly inside the same classInternal helpers and secrets
protectedThe class and its child classesShared helpers in a base page
readonlyRead anywhere, set only in the constructorLocators, URLs
class BankAccount {
  readonly accountNumber: string;
  private balance = 0;

  constructor(accountNumber: string) {
    this.accountNumber = accountNumber;
  }

  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Amount must be positive');
    this.balance += amount;
  }

  getBalance(): number {
    return this.balance;
  }
}

const acc = new BankAccount('SB-1001');
acc.deposit(500);
console.log(acc.getBalance()); // 500
// acc.balance = 1000000;      // ❌ Error: private

🧬 Inheritance with extends and super

Inheritance lets one class reuse another class. The child class gets every property and method of the parent and can add or override its own. Call super() in the child constructor to run the parent constructor first.

class Employee {
  constructor(public name: string, protected salary: number) {}

  getDetails(): string {
    return this.name + ' earns ' + this.salary;
  }
}

class Manager extends Employee {
  constructor(name: string, salary: number, public teamSize: number) {
    super(name, salary); // must come first
  }

  // Override the parent method
  getDetails(): string {
    return super.getDetails() + ' and manages ' + this.teamSize + ' people';
  }
}

const m = new Manager('Neha', 150000, 8);
console.log(m.getDetails()); // Neha earns 150000 and manages 8 people

🧰 Getters, Setters and Static Members

Getters and setters look like properties but run code. Static members belong to the class itself, not to each object. They are handy for shared constants and utility methods.

class Temperature {
  private celsius = 0;

  get fahrenheit(): number {
    return this.celsius * 9 / 5 + 32;
  }

  set value(c: number) {
    if (c < -273.15) throw new Error('Below absolute zero');
    this.celsius = c;
  }
}

const t = new Temperature();
t.value = 25;
console.log(t.fahrenheit); // 77

class TestUtils {
  static readonly DEFAULT_TIMEOUT = 30000;

  static randomEmail(): string {
    return 'user' + Date.now() + '@test.com';
  }
}

console.log(TestUtils.DEFAULT_TIMEOUT); // no "new" needed
console.log(TestUtils.randomEmail());

🧱 Abstract Classes

An abstract class cannot be created directly with new. It is a base for other classes and can force them to implement certain methods. This is useful for a BasePage that every page must extend.

abstract class Shape {
  abstract area(): number; // child classes MUST implement this

  describe(): string {
    return 'Area is ' + this.area().toFixed(2);
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area(): number { return Math.PI * this.radius ** 2; }
}

console.log(new Circle(2).describe()); // Area is 12.57
// new Shape(); // ❌ Error: cannot create an instance of an abstract class

🤝 Classes and Interfaces Together

A class can implement an interface. The interface describes *what* must exist and the class provides *how* it works. If the class misses a member, TypeScript reports an error.

interface Reporter {
  report(testName: string, passed: boolean): void;
}

class ConsoleReporter implements Reporter {
  report(testName: string, passed: boolean): void {
    console.log((passed ? '✅ ' : '❌ ') + testName);
  }
}

Classes vs interfaces in one line: an interface is a compile-time contract with no code, while a class is real code that creates objects at runtime.


🎭 Classes in Playwright: Page Object Model

Here is a small but realistic Page Object Model built with everything you learned: readonly locators, a constructor, a protected base helper, inheritance and public action methods.

1. Base page

// pages/BasePage.ts
import { Page } from '@playwright/test';

export abstract class BasePage {
  constructor(protected readonly page: Page) {}

  abstract readonly path: string;

  async open(): Promise<void> {
    await this.page.goto('https://www.saucedemo.com' + this.path);
  }
}

2. Login page

// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  readonly path = '/';
  readonly username: Locator;
  readonly password: Locator;
  readonly loginButton: Locator;
  readonly error: Locator;

  constructor(page: Page) {
    super(page);
    this.username = page.getByPlaceholder('Username');
    this.password = page.getByPlaceholder('Password');
    this.loginButton = page.getByRole('button', { name: 'Login' });
    this.error = page.locator('[data-test="error"]');
  }

  async login(user: string, pass: string): Promise<void> {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.loginButton.click();
  }
}

3. The test stays short and readable

// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('valid user can log in', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('standard_user', 'secret_sauce');
  await expect(page).toHaveURL(/inventory/);
});

test('locked user sees an error', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('locked_out_user', 'secret_sauce');
  await expect(loginPage.error).toContainText('locked out');
});

Go deeper with Playwright Page Object Model step by step and Playwright Custom Fixtures, which can create page objects for you automatically.


✅ Best Practices

  • One class per page or component; keep each class focused on one responsibility (see SOLID principles with Playwright).
  • Mark locators readonly so they cannot be reassigned by accident.
  • Keep assertions in tests, and keep actions in page classes.
  • Use private for helpers the tests should not call directly.
  • Prefer composition (a page that *has* a header component) over deep inheritance chains.

❓ FAQ

🤔 What is the difference between a class and an object in TypeScript?

A class is the template; an object is a real value created from it with new. One class can create many objects.

🤔 What is a constructor in TypeScript?

A special method named constructor that runs automatically when you create an object with new. It sets the starting values of properties.

🤔 When should I use a class instead of an interface?

Use a class when you need real behavior and to create objects at runtime, such as page objects. Use an interface when you only need to describe a shape or contract.

🤔 Why do Playwright page objects use classes?

Classes group a page’s locators and actions in one place, so tests read like user steps and a UI change only needs one fix in the page class.

🤔 What does super() do?

In a child class constructor, super() calls the parent class constructor. It must run before you use this.


🎯 Next Steps

🎬 Watch the full TypeScript tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

2 thoughts on “TypeScript Classes for Beginners: Constructors, Inheritance, Access Modifiers and Page Objects”

Leave a Comment