Where this sits
Last session covered abstraction and interfaces. This one closes TypeScript. Next class is Playwright fundamentals, and everything from here uses the language work already done.
Announced in class, and there is a lot of it. Tomorrow, 9 AM IST: a live test mixing JavaScript and TypeScript, one hour, in a 12 hour window until 9 PM, results posted in the SDET Club thread. Before Tuesday: exercises to 215, repository pushed. Certifications: Introduction to MCP next week, then Advanced MCP and AI Capability and Limitations, working toward eight or nine in total. Extra masterclasses this month: Playwright CLI, Playwright MCP, Playwright AI agents, Selenium to Playwright migration in two parts, and Regex 101. A Playwright hackathon is being planned on the same format as the AI batch, 24 hours, cash prizes.
Enums, for the strings you keep retyping
An enum is a set of named constants. The problem it solves is magic strings: "PASS", "FAIL", "SKIP" scattered across a suite, each one a typo waiting to happen.
enum TestStatus {
PASS = "PASS",
FAIL = "FAIL",
SKIP = "SKIP",
}
console.log(TestStatus.PASS);
Now there is one place the value lives. Rename it and every use follows.
Where they earn their place in a framework:
| Enum | Members |
|---|---|
| Test status | PASS, FAIL, SKIP, PENDING, BLOCKED |
| Environment | DEV, QA, STAGING, PROD |
| Browser | CHROME, FIREFOX, SAFARI |
| Severity | LOW, MEDIUM, HIGH, CRITICAL |
| HTTP method | GET, POST, PUT, PATCH, DELETE |
The browser one goes straight into a launcher:
enum Browser {
CHROME = "chromium",
FIREFOX = "firefox",
SAFARI = "webkit",
}
function launch(browser: Browser) {
switch (browser) {
case Browser.CHROME:
return "launching chromium";
case Browser.FIREFOX:
return "launching firefox";
}
}
The point made in class about this shape is the one worth keeping: the key never changes, only the value does. Every call site says Browser.CHROME. If the underlying string has to change, you edit one line and nothing else moves.
Asked in class: is an enum the same as a properties file? No. A properties file holds values that can change between runs and can be loaded dynamically. An enum is compiled in and constant. They look similar and solve different halves of the problem: enum for a fixed set the code branches on, properties or environment variables for values that differ per environment.
Generics, for the function that should not care
TypeScript is strict about types, which is the point of it. But sometimes the type is genuinely not known when you write the function.
function getFirst(items: string[]): string {
return items[0]!;
}
That works for strings and nothing else. Write it again for numbers and you have duplicated a function for no reason.
A generic puts a placeholder where the type goes:
function getFirst<T>(items: T[]): T {
return items[0]!;
}
const code: number = getFirst([200, 404, 500]);
const page: string = getFirst(["login", "cart"]);
Both compile. T is not a type, it is a slot, filled in at the call site. T is only a convention, and the session made the point directly: you could name it anything, it is just a placeholder.
Generic classes work the same way, and generics are heavily used on API responses, where the envelope is known and the payload is not:
interface ApiResponse<T> {
statusCode: number;
data: T;
}
The status code is always a number. What data holds depends on the endpoint.
Why not just use any
This came up in class and the answer deserves proving.
Assign a number array's first element to a string and the generic version refuses:
const bad: string = getFirst([200, 404]);
error TS2322: Type 'number' is not assignable to type 'string'.
The any version of the same line compiles without complaint and fails when it runs. Both give you reuse. Only the generic keeps the safety.
public, private, protected
Three access modifiers, and the one that matters most for a page object model is the middle one.
| Modifier | Reachable from | Use it for |
|---|---|---|
public | Anywhere. The default | The methods a test actually calls |
protected | The class and its subclasses | Base page helpers that page objects need and tests should not touch |
private | The declaring class only | Secrets and internals. Reachable outside only through a getter |
One correction, because it changes what you can rely on. The session described protected as also covering the same folder or package, with a neighbour able to reach into the house next door. That is a Java habit, and TypeScript has no packages at all. The compiler is explicit:
error TS2445: Property 'timeout' is protected and only accessible within class 'BasePage' and its subclasses.A sibling class sitting in the same folder is refused, exactly like code anywhere else. Access follows inheritance, not directory layout. The conclusion drawn in class is still correct, that
protectedis the right choice for a base page, because page objects extend it. Only the reason was misstated, and it matters the moment you write a helper class that is not a subclass and wonder why it cannot see anything.
private is not reachable from a subclass either, which is the difference between the two:
class BasePage { private apiKey = "secret"; }
class LoginPage extends BasePage { probe() { return this.apiKey; } }
error TS2341: Property 'apiKey' is private and only accessible within class 'BasePage'.
In practice a base page ends up mostly protected: shared helpers the page objects inherit, hidden from the tests.
readonly sits alongside these and was covered last session: set once at construction, never reassigned.
Also flagged: let, const and var are not class field declarations. Inside a class body you declare fields directly, with an optional modifier. Writing let timeout = 500 in a class body is not the same thing and is not what you want.
Abstract classes
An abstract class is one that is deliberately incomplete. It can hold real implementations and constructors, but any member marked abstract has no body, and a subclass has to supply one.
abstract class BaseTest {
abstract setup(): void;
run() { console.log("shared"); }
}
const t = new BaseTest();
class ApiTest extends BaseTest {}
error TS2511: Cannot create an instance of an abstract class.
error TS2515: Non-abstract class 'ApiTest' does not implement inherited
abstract member setup from class 'BaseTest'.
Two rules, both enforced: you cannot instantiate it, and a subclass must complete it.
| Abstract class | Interface | |
|---|---|---|
| Can hold implementations | Yes | No |
| Constructor | Yes | No |
| Access modifiers | Yes | No, everything is public |
| Survives compilation | Yes | No, erased |
| A class relates to it with | extends | implements |
The honest note from class: abstract classes are rarely used in automation. Know them for the interview question, do not go looking for a place to use one.
Which of these are real at runtime
The last row of that table is worth seeing rather than believing, and one compile settles all three of today's topics at once.
interface Shape { area(): number; }
abstract class BaseTest { abstract setup(): void; run() { console.log("shared"); } }
enum TestStatus { PASS = "PASS", FAIL = "FAIL" }
class ApiTest extends BaseTest { setup() { console.log(TestStatus.PASS); } }
new ApiTest().setup();
Compiled output, in full:
"use strict";
class BaseTest {
run() { console.log("shared"); }
}
var TestStatus;
(function (TestStatus) {
TestStatus["PASS"] = "PASS";
TestStatus["FAIL"] = "FAIL";
})(TestStatus || (TestStatus = {}));
class ApiTest extends BaseTest {
setup() { console.log(TestStatus.PASS); }
}
new ApiTest().setup();
The interface is gone. The abstract class became an ordinary class, with setup dropped because it never had a body. And the enum became a real object, which is why you can read TestStatus.PASS while the program runs.
That last one is the practical difference: an enum is the only one of the three you can inspect, log or iterate at runtime.
Tasks and announcements
Two deadlines. A live JavaScript and TypeScript test tomorrow, released 9 AM IST, open 12 hours until 9 PM, one hour to complete, results posted in the SDET Club thread. And exercises to 215 with your repository pushed before Tuesday, because Tuesday starts Playwright fundamentals and nothing from the language work should still be outstanding.
- Next class: Playwright fundamentals. Everything learned so far now gets used rather than taught.
- Certifications: Introduction to MCP next week, Tuesday or Wednesday evening, then Advanced MCP and AI Capability and Limitations. Around eight or nine in total by the end.
- Extra masterclasses this month, mandatory for this batch: Playwright CLI, Playwright MCP, Playwright AI agents, Selenium to Playwright migration parts one and two, and Regex 101. An Agent Factory session follows for anyone whose framework is finished.
- A Playwright hackathon is being planned on the AI batch's format: 24 hours, a coding and GitHub challenge, cash prizes.
- Worth doing if you have not: publish four or five of your own skills to the QA Skills repo, which sees around 38,000 users a month.