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.
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.
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:
class F1 {}
class F2 {}
class Son extends F1, F2 {}
class F1{}; class F2{}; class Son extends F1, F2 {}
^
SyntaxError: Unexpected token ','
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.
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);
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.
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.
class BaseTest {
setup() { console.log('base setup'); }
}
class ApiTest extends BaseTest {
setup() { console.log('api setup'); }
}
new ApiTest().setup();
new BaseTest().setup();
api setup
base setup
There is no @Override annotation as there is in Java. You just declare the method again.
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.
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'));
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.
The super chain, worked
An exercise from the session, and the kind that gets asked because it looks harder than it is.
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());
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:
class D extends B {}
console.log(new D().who());
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.
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.
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.
npm install -D typescript
npx tsc --version
npx tsc --init
tsc --init writes a tsconfig.json. The three lines worth looking at:
"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.
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:
enum Severity { Low, High }
class Bug {
constructor(private title: string, public sev: Severity) {}
}
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.
tsc compiles and stops. A runner compiles in memory and executes, leaving no .js behind.Type annotations, the whole vocabulary so far
An annotation goes in three places: on a variable, on each parameter, and on the return.
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 |
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.
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.
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:
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.
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 is25_TypeScript. - Install
tsxbefore you start the homework.npm i -D tsx, thennpx tsx yourfile.ts. It saves thets-nodefailure 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.