The Testing Academy · Class Notes Tuesday, 1 September (IST)
Live class · study guide

Polymorphism, and the TypeScript setup that breaks on a fresh install

The last JavaScript-only session. Which inheritance shapes are allowed and which two are not, mixins as the workaround you will never actually type, and polymorphism, which in JavaScript means exactly one thing: method overriding. Then the switch every file makes from here on, into TypeScript and type annotations.

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 every snippet here was reconstructed from the session and then executed before publishing, on Node v22.22.3 with TypeScript 7.0.2. Two things the room was told turned out to behave differently on a machine set up today; both are flagged in place.

01

Where this sits

Last session finished encapsulation and inheritance. This one closes the OOP run with the inheritance shapes and polymorphism, then starts TypeScript. Today's exercises run to lab 196.

This is the last JavaScript-only class. From here every file is .ts.

02

Five shapes, and the two JavaScript will not give you

The question in an interview is almost always phrased the same way: what are the types of inheritance, and which ones does JavaScript support. There are five worth naming.

Supported, all through extends Single Animal Dog Multi-level BasePage AuthPage AdminPage Hierarchical BasePage Login Cart one parent, as many children as you like Not supported Multiple F1 F2 Son two parents, one child Hybrid any mix that contains multiple, so it inherits the same problem class Son extends F1, F2 is a SyntaxError, not a runtime failure
Single, multi-level and hierarchical all work through extends. Multiple and hybrid do not, and hybrid only fails because it contains multiple.

The rule to carry into an interview is short. Single, multi-level and hierarchical are supported, all with the same extends keyword. Multiple is not, and hybrid is not, because hybrid always contains a multiple.

One father can have as many children as you like, so hierarchical is fine. One child cannot have two fathers, so multiple is not. That was the analogy used in the room, and it survives the interview.

Worth knowing that the failure is not subtle. Writing two parents is rejected before anything runs:

Code
class F1 {}
class F2 {}
class Son extends F1, F2 {}
Code
class F1{}; class F2{}; class Son extends F1, F2 {}
                                            ^
SyntaxError: Unexpected token ','
03

Mixins, the workaround you will never type

Multiple inheritance does have a way in, and it has a name: mixins. A mixin is a function that takes a class and returns a new class extending it. Because it is a function, you can nest calls, and a class can end up with behaviour from several sources.

Code
const LoginMixin = (Base) => class extends Base { login() { return 'login'; } };
const ShotMixin  = (Base) => class extends Base { shot()  { return 'shot';  } };

class TestCase { run() { return 'run'; } }

class SmartTest extends LoginMixin(ShotMixin(TestCase)) {}

const s = new SmartTest();
console.log(s.run(), s.login(), s.shot());
console.log(s instanceof TestCase);
Code
run login shot
true

It works, and instanceof TestCase still holds, so the chain is real inheritance rather than object copying.

The session was direct about this: the syntax is strange and you are not going to use it in automation. Nobody writes mixins in a Playwright framework, and interviewers rarely ask. Learn the word and the shape so the question does not surprise you, then move on. It is here for completeness, not for your framework.

04

Polymorphism means one thing here

Polymorphism is one object behaving differently depending on what it actually is. The analogy from the room: you speak to your mother, your friends and your colleagues in three different ways, and you are still one person.

In a language like Java that splits two ways:

Kind Also called In JavaScript
Method overriding Runtime polymorphism Supported
Method overloading Compile-time polymorphism Not supported

Only overriding exists in JavaScript. A child class declares a method with the same name as its parent, and which one runs is decided by the object you created.

Code
class BaseTest {
  setup() { console.log('base setup'); }
}

class ApiTest extends BaseTest {
  setup() { console.log('api setup'); }
}

new ApiTest().setup();
new BaseTest().setup();
Code
api setup
base setup

There is no @Override annotation as there is in Java. You just declare the method again.

obj.setup() a call arrives Does the object's own class declare setup? not the variable, the object yes no the child's setup runs the override wins walk up to the parent and keep looking Nothing anywhere up the chain is a TypeError
Dispatch follows the object, not the variable. If the object's class does not declare the method, the lookup walks up the prototype chain.
05

Overloading does not error, it silently wins

This is the part worth slowing down on, because "not supported" makes it sound like JavaScript will stop you. It will not.

Code
class BaseTest {
  setup(a)    { return `one-arg: ${a}`; }
  setup(a, b) { return `two-arg: ${a},${b}`; }
}

const t = new BaseTest();
console.log(t.setup('x'));
console.log(t.setup('x', 'y'));
Code
two-arg: x,undefined
two-arg: x,y

The second declaration replaced the first. There is one setup on the prototype, not two, and calling it with one argument runs the two-argument version with b as undefined. No warning, no error.

This is why "JavaScript has no overloading" matters in practice and not only in interviews. Two people adding a same-named method to the same page object will not get a merge conflict or a runtime error. The file will simply do the wrong thing, and the symptom shows up somewhere else entirely. If you want different behaviour for different arguments, branch inside one method or use default parameters.

06

The super chain, worked

An exercise from the session, and the kind that gets asked because it looks harder than it is.

Code
class A { who() { return 'A'; } }
class B extends A { who() { return 'B > ' + super.who(); } }
class C extends B { who() { return 'C > ' + super.who(); } }

console.log(new C().who());
Code
C > B > A

Read it as a chain rather than a puzzle. new C() runs C's who, which prints C and then hands off to B with super.who(), which prints B and hands off to A. The answer is C, then B, then A.

The follow-up is the useful half. Drop the override and the parent runs instead:

Code
class D extends B {}
console.log(new D().who());
Code
B > A

That is the fallback rule from the dispatch diagram, doing exactly what it says.

Interview shape: what is the difference between inheritance and overriding? Inheritance is the relationship, a child class acquiring the properties and methods of a parent. Overriding is one thing you can do with that relationship, replacing a parent's method in the child. Polymorphism is the effect you get: the same call behaves differently depending on which object is on the other end.

07

TypeScript is JavaScript plus rules

The switch happens here, and the framing from the room is the one to keep: TypeScript is not a different language. It is JavaScript with rules layered on. Same person, wearing a jacket.

Every .ts file ends up as JavaScript. That is not an implementation detail you can ignore, it is the whole design. If a rule cannot be expressed in JavaScript, TypeScript cannot enforce it at runtime.

The one concrete thing a type annotation buys you: let timeout = 5 leaves the reader guessing whether 5 is a number or a string. let timeout: number = 5 does not.

08

Getting a .ts file to actually run

Three commands were shown, and this is where a fresh install today diverges from the machine used in class.

Code
npm install -D typescript
npx tsc --version
npx tsc --init

tsc --init writes a tsconfig.json. The three lines worth looking at:

Code
"module": "nodenext",
"target": "esnext",
"strict": true

Ignore the rest of that file for now. It matters when Playwright arrives, not before.

Then there are two different jobs, and they are easy to confuse:

Command What it does Leaves a .js file?
tsc lab184.ts Compiles only. Nothing runs Yes
ts-node lab184.ts Compiles and runs, in memory No

Compiling by hand shows what TypeScript actually produces. The emitted file opens with "use strict", which is the "follow the rules" switch turned on in plain JavaScript.

The version trap. ts-node was the command given in class, and it works on the setup used there, which reported TypeScript 6.3. Installing today is different: npm install -D typescript now brings TypeScript 7.0.2, and ts-node 10.9.2 crashes against it before running a line of your code. The crash is inside ts-node itself, so it is not your file being wrong.

Code
TypeError: Cannot read properties of undefined (reading 'fileExists')
    at readConfig (.../ts-node/dist/configuration.js:91:33)

Three ways out, all checked on Node v22.22.3:

Fix Command Verdict
Use tsx instead npm i -D tsx then npx tsx lab184.ts Works. Handles everything the syllabus reaches next
Pin TypeScript to 6 npm i -D typescript@6 ts-node Works, TypeScript 6.0.3, ts-node behaves as taught
Node's own stripper node --experimental-strip-types lab184.ts Works today, but see below

The Node flag is the tempting one and it is the one to be careful with. It removes type annotations and runs what is left, so it cannot handle TypeScript that is more than annotations:

Code
enum Severity { Low, High }
class Bug {
  constructor(private title: string, public sev: Severity) {}
}
Code
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX

enum and constructor parameter properties are exactly what the next class covers, along with private, public, protected and abstract classes. tsx runs that file without complaint. Install tsx now and you will not have to change tooling mid-topic.

lab184.ts what you write tsc tsx lab184.js written to disk "use strict"; plain JavaScript, nothing ran you would still have to node lab184.js compiled in memory nothing on disk output in your terminal one step, and the folder stays clean
Two different jobs. tsc compiles and stops. A runner compiles in memory and executes, leaving no .js behind.
09

Type annotations, the whole vocabulary so far

An annotation goes in three places: on a variable, on each parameter, and on the return.

Code
let testName: string = "login test";

function add(a: number, b: number): number {
  return a + b;
}

function sayHello(message: string): void {
  console.log(`Hi ${message}`);
}

const multiply = (a: number, b: number): number => a * b;

Arrow functions annotate exactly the same way. Nothing special about them.

The types covered:

Type Use it for
string Text
number Every number. There is no separate float or int
boolean True and false
null, undefined Exactly what they say
string[] or Array<string> Arrays, both spellings valid
void A function that returns nothing
never A function that never returns at all
any You do not know yet. Avoid
unknown You do not know yet, but safely. Avoid
10

void, never, any and unknown

void against never is the pair that gets asked. void means the function finishes and hands nothing back. never means the function does not finish: it throws every time, or it loops forever.

Code
function log(msg: string): void {
  console.log(msg);
}

function fail(msg: string): never {
  throw new Error(msg);
}

Empty return is void. No return, ever, is never. never is rare in test code and it is worth recognising rather than reaching for.

any against unknown is the pair that matters more day to day, because one of them turns the type checker off.

Code
let a: any = "hello";
a.definitelyNotAMethod();

let u: unknown = "hello";
u.definitelyNotAMethod();

any compiles clean and then explodes at runtime. unknown is rejected at compile time, which is the entire point of it. To use an unknown you have to prove what it is first:

Code
let u: unknown = "hello";

if (typeof u === "string") {
  console.log(u.toUpperCase());
}

Inside the if, TypeScript knows it is a string and lets you call string methods. That is called narrowing.

One precision worth carrying, because it is easy to hear it the other way round. The variable's declared type does not change. u is unknown before the if, narrowed to string inside the block, and unknown again after it. Same for any: assigning a string to an any does not turn it into a string, it stays any and you can still assign a number to it afterwards. The check narrows what TypeScript will let you do in that block, it does not rewrite the declaration.

Both are escape hatches. unknown is the safe one because it forces the check. Reach for a real type first.

11

Tasks and announcements

Today's task. Replicate the exercises through lab 196. Everyone should reach 196.

  • From here every file is .ts. Not .js. The folder for TypeScript work is 25_TypeScript.
  • Install tsx before you start the homework. npm i -D tsx, then npx tsx yourfile.ts. It saves the ts-node failure above and it will still be the right tool next class.
  • Next class continues TypeScript: interfaces, enums, generics, private, public, protected, readonly, abstract classes, type assertions, decorators and tuple types. Abstraction is covered there rather than in JavaScript.
  • This week's target is to finish TypeScript and go straight into Playwright.
  • Weekly test: released Sundays at 9 AM and open for 12 hours, so you can take it whenever suits you inside that window. JavaScript parts 3 and 4 are next.
  • The OOPs interview questions in the Playwright section of the portal are worth marking off as you learn each one.