The Testing Academy · Class Notes Wednesday, 3 September (IST)
Live class · study guide

Abstraction, interfaces, and the readonly that does not actually lock anything

The last big TypeScript idea before Playwright. Abstraction against encapsulation, which are not the same thing and get confused constantly, then interfaces in depth: the contract that forces every object into the same shape, readonly, optional fields, and the difference between extends and implements. Exercise 200 landed in this session.

By Pramod Dutta, The Testing Academy. Study notes from the live Playwright 3x class, rebuilt from the session recording. The Eraser deck was not reachable while these notes were written, so the code was reconstructed from the session and then compiled before publishing, on TypeScript 7.0.2. Every error message quoted here is the real one the compiler prints. One consequence the session did not draw out, about what readonly does at runtime, is flagged in place.

01

Where this sits

Last session covered polymorphism and the start of TypeScript. This one finishes the OOP run with abstraction, which in TypeScript mostly means interfaces.

JavaScript is done. TypeScript is nearly done. Playwright proper starts next, and the session was explicit that everything after this point moves fast because the language work is behind us.

Announced in class. Extra evening sessions this month, roughly one hour, covering Playwright CLI, Playwright MCP, Playwright AI agents, using Cursor with Playwright, and sharding across containers. They are additional to the morning classes, recorded, and the ask was that nobody skip them. Also coming: about five practice questions a week with solutions, and an Introduction to MCP session with certification, timing to be confirmed.

02

Abstraction is not encapsulation

These two get mixed up more than any other pair in OOP, because both are described as "hiding". The distinction that actually separates them is what level they operate at.

Encapsulation: inside one class class Person #name #age get set the only two doors in outside code hides data, within a class the lock on the bonnet Abstraction: across classes car.start() all the user sees Engine Gearbox Fuel never shown to the caller hides complexity, at the class level: the steering wheel
Same word, two levels. Encapsulation controls who reaches a field inside one class. Abstraction hides whole collaborating classes behind one call.
EncapsulationAbstraction
HidesDataComplexity
OperatesWithin a classAcross classes
Built withPrivate fields, getters, settersInterfaces, enums, abstract classes
AnswersWho is allowed to touch this?What does the caller need to see?
In a carThe lock on the bonnetThe steering wheel

The analogy that landed in the room: say "we are all Indian" and you have hidden every individual behind one category, which is abstraction. Ask one person for access to their family and they refuse, which is encapsulation. One is about level of description, the other about permission.

Both appear constantly in Playwright. A page object encapsulates its locators and abstracts the page it drives.

03

Three ways to build abstraction

In TypeScript there are three: interfaces, enums, and abstract classes. This session was almost entirely the first one, because it is the one you will use every day.

Worth being precise about a claim made in passing. JavaScript is not incapable of OOP: it has classes, inheritance, and genuinely private # fields. What it lacks is a way to enforce a contract at compile time. interface, abstract and the access modifiers are TypeScript additions that exist to check your code before it runs. That is the gap TypeScript fills, rather than OOP as a whole.

04

The problem interfaces solve

Without a contract, nothing forces two objects to look alike:

Code
const user1 = { name: "John", age: 30, email: "abc@gmail.com" };
const user2 = { name: "Jane", email: "jane@gmail.com" };
const user3 = { age: 25, email: "sam@gmail.com" };

Three users. One has no age, one has no name. Perfectly valid JavaScript, and a mess to consume, because any code reading these has to defend against every field being absent.

An interface is a template that objects are forced to match:

Code
interface User {
  name: string;
  age: number;
  email: string;
}

const user2: User = { name: "John", email: "abc@gmail.com" };
Code
error TS2741: Property 'age' is missing in type
'{ name: string; email: string; }' but required in type 'User'.

The error arrives before anything runs. That is the whole value: the interface forces the shape, so consumers can rely on it.

No contract name age email user1, complete name ---- email user2, no age ---- age email user3, no name every consumer must defend against every missing field interface User the contract name, age, email Contract enforced name age email user1 name age email user2, or it will not compile name age email user3, or it will not compile one shape, so consumers can simply read the fields
The interface does not add behaviour. It removes the possibility of a wrong shape.
05

readonly, and what it does not do

readonly says a field can be set once, at creation, and never reassigned.

Code
interface Point {
  readonly x: number;
  readonly y: number;
}

const p: Point = { x: 10, y: 20 };
p.x = 99;
Code
error TS2540: Cannot assign to 'x' because it is a read-only property.

The session's argument for it is a good one: page object locators should be readonly. You set a selector once where it is declared, and no other class gets to reassign it. If a locator changes, you change it in the one place it was declared, which is what you wanted anyway.

The part worth adding, which follows from something else said in the same session. Interfaces vanish at compile time. They exist only for type checking, and produce no JavaScript. So readonly is a compile-time contract, not a runtime lock. Compile the file and the guarantee is gone: plain JavaScript will happily reassign that property, and any value arriving from JSON.parse or an untyped library was never checked at all.

If you need the value genuinely immutable while the program runs, that is Object.freeze, which throws on assignment in strict mode. readonly protects you from your teammates. Object.freeze protects you from your program.

Here is the erasure, on a file with two interfaces in it:

Code
interface Point { readonly x: number; readonly y: number; }
interface Calculator { add(a: number, b: number): number; }
const p: Point = { x: 10, y: 20 };
const calc: Calculator = { add: (a, b) => a + b };
console.log(p.x, calc.add(2, 3));

Compiled output, in full:

Code
"use strict";
const p = { x: 10, y: 20 };
const calc = { add: (a, b) => a + b };
console.log(p.x, calc.add(2, 3));

Both interfaces are gone. Not minified, not inlined. Gone.

06

Optional fields, with a question mark

Not every field is mandatory. A ? marks one optional, and the API example from class is the natural one: a POST has a body, a GET often does not.

Code
interface ApiResponse {
  statusCode: number;
  body: string;
  headers?: object;
  responseTime: number;
}

const withHeaders: ApiResponse = { statusCode: 200, body: "", headers: {}, responseTime: 200 };
const noHeaders:   ApiResponse = { statusCode: 200, body: "", responseTime: 200 };

Both compile. The second omits headers and that is fine, because the ? said it could.

The real-world example from the session is a config split: a local run needs no timeout or retries, a CI run does, so those two fields are optional and one interface covers both.

Asked in class: if a field is optional and you do not set it, what do you get when you read it? undefined. Not null, not an error. Reading it is allowed, which is exactly why the type is number | undefined and TypeScript will make you handle the undefined case before you use it.

07

Methods in an interface

An interface can declare a method. It declares only the signature, never the body, so whoever adopts the interface has to supply the implementation.

Code
interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

const calc: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
};

Drop subtract and it will not compile. That is the point: if your team agrees a calculator has exactly two operations, the interface enforces it on everyone who builds one, rather than relying on a code review to catch it.

08

extends against implements

This is the pair to memorise, and it is the same rule as Java.

interface extends interface interface BasePage extends interface LoginPage a template building on a template class implements interface interface Executable implements class TestCase real code fulfilling the template An interface cannot use implements. TypeScript has a dedicated error for it: TS1176.
Interfaces extend each other. Classes implement them. There is no third combination.
Code
interface BasePage { url: string; title: string; }
interface LoginPage extends BasePage { username: string; password: string; }

A LoginPage now demands all four fields, its own two and the two it inherited. Miss one and you get the same missing-property error as before.

A class works the other way:

Code
interface Executable { run(): void; getStatus(): string; }

class TestCase implements Executable {
  run(): void { console.log("running"); }
}
Code
error TS2420: Class 'TestCase' incorrectly implements interface 'Executable'.
  Property 'getStatus' is missing in type 'TestCase' but required in type 'Executable'.

And the combination that does not exist, exactly as the session said:

Code
interface B implements A { y: number; }
Code
error TS1176: Interface declaration cannot have 'implements' clause.

TypeScript has a specific error for it, which tells you people try it often enough to be worth naming.

Interview shape: when do you use extends and when implements? Interface to interface is extends. Class to interface is implements. A class extending another class is also extends. The rule is that implements only ever appears between a class and an interface, and only in that direction.

09

Index signatures

Last shape from the session, and one you will meet in Playwright configs and header maps: a type where the keys are not known in advance but every value has the same type.

Code
interface StringDictionary {
  [key: string]: string;
}

const headers: StringDictionary = { hello: "world", foo: "bar" };
const bad: StringDictionary = { hello: 42 };
Code
error TS2322: Type 'number' is not assignable to type 'string'.

Any key you like, as long as the value is a string.

10

Tasks and announcements

Exercises are at 200. The session crossed the 200 mark live. Make sure your GitHub repository has all of them, and if you have fallen behind, this week is the time to catch up: recordings, doubt threads and task threads are all available.

  • Coming this month: extra one-hour evening sessions on Playwright CLI, Playwright MCP, Playwright AI agents, Cursor with Playwright, and sharding across containers. Recorded, but the ask was to attend live.
  • Introduction to MCP, with certification, is being scheduled for an evening this week. If you attended the Agent Skills session, the recording is in SDET Club and the certificate link follows.
  • About five practice questions a week, with solutions, starting now.
  • The interview question bank in the portal has over 1,500 questions, and marking them off earns leaderboard points.
  • Not being covered separately: Java-style collections such as Set and Map as a topic. Arrays, map and filter are already covered and are what automation actually uses.