The Testing Academy · Class Notes Saturday, 5 September (IST)
Live class · study guide

Enums, generics and access modifiers, and which of the three survives compilation

The last TypeScript session. Enums for the constants scattered through your suite, generics for the function that should not care about the data type, and public, private and protected, which decide what a base page may hand to a page object. One compile of all three shows which are real at runtime and which were never there.

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 compiled before publishing, on TypeScript 7.0.2. Every error message quoted is the compiler's own. One analogy used for protected does not hold in TypeScript, and the correction is flagged where it matters.

01

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.

02

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.

Code
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:

EnumMembers
Test statusPASS, FAIL, SKIP, PENDING, BLOCKED
EnvironmentDEV, QA, STAGING, PROD
BrowserCHROME, FIREFOX, SAFARI
SeverityLOW, MEDIUM, HIGH, CRITICAL
HTTP methodGET, POST, PUT, PATCH, DELETE

The browser one goes straight into a launcher:

Code
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.

03

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.

Code
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:

Code
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:

Code
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.

any number[] in string out no relationship between the two compiles clean, then throws at runtime the check is simply off generic T T[] in T out the same T on both sides, whatever it turns out to be a mismatch is refused before the code runs reusable and still checked Both are reusable. Only one still tells you when you are wrong.
The difference is not reuse, it is whether the compiler still knows what came out.

Assign a number array's first element to a string and the generic version refuses:

Code
const bad: string = getFirst([200, 404]);
Code
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.

04

public, private, protected

Three access modifiers, and the one that matters most for a page object model is the middle one.

class BasePage public baseUrl protected timeout private apiKey class LoginPage extends BasePage a subclass public, protected private: no class Helper same folder, not a subclass public only protected: refused any other code public only
Reach is decided by inheritance, not by where the file sits. A sibling class in the same folder gets no more access than code anywhere else.
ModifierReachable fromUse it for
publicAnywhere. The defaultThe methods a test actually calls
protectedThe class and its subclassesBase page helpers that page objects need and tests should not touch
privateThe declaring class onlySecrets 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 protected is 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:

Code
class BasePage { private apiKey = "secret"; }
class LoginPage extends BasePage { probe() { return this.apiKey; } }
Code
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.

05

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.

Code
abstract class BaseTest {
  abstract setup(): void;
  run() { console.log("shared"); }
}

const t = new BaseTest();
class ApiTest extends BaseTest {}
Code
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 classInterface
Can hold implementationsYesNo
ConstructorYesNo
Access modifiersYesNo, everything is public
Survives compilationYesNo, erased
A class relates to it withextendsimplements

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.

06

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.

Code
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:

Code
"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();
You write The browser gets interface Shape abstract class BaseTest enum TestStatus nothing at all, erased completely class BaseTest, abstract member dropped a real object you can read at runtime shipped shipped gone
Three constructs, three fates. Only the interface leaves nothing behind.

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.

07

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.