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.
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 | Abstraction | |
|---|---|---|
| Hides | Data | Complexity |
| Operates | Within a class | Across classes |
| Built with | Private fields, getters, setters | Interfaces, enums, abstract classes |
| Answers | Who is allowed to touch this? | What does the caller need to see? |
| In a car | The lock on the bonnet | The 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.
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.
The problem interfaces solve
Without a contract, nothing forces two objects to look alike:
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:
interface User {
name: string;
age: number;
email: string;
}
const user2: User = { name: "John", email: "abc@gmail.com" };
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.
readonly, and what it does not do
readonly says a field can be set once, at creation, and never reassigned.
interface Point {
readonly x: number;
readonly y: number;
}
const p: Point = { x: 10, y: 20 };
p.x = 99;
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.readonlyprotects you from your teammates.Object.freezeprotects you from your program.
Here is the erasure, on a file with two interfaces in it:
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:
"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.
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.
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.
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.
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.
extends against implements
This is the pair to memorise, and it is the same rule as Java.
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:
interface Executable { run(): void; getStatus(): string; }
class TestCase implements Executable {
run(): void { console.log("running"); }
}
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:
interface B implements A { y: number; }
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.
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.
interface StringDictionary {
[key: string]: string;
}
const headers: StringDictionary = { hello: "world", foo: "bar" };
const bad: StringDictionary = { hello: 42 };
error TS2322: Type 'number' is not assignable to type 'string'.
Any key you like, as long as the value is a string.
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
SetandMapas a topic. Arrays,mapandfilterare already covered and are what automation actually uses.