Before you begin
Why this matters
Classes often extend other classes, and a method can be written in several of them. Knowing how JavaScript finds a method lets you predict which version runs, and spot a quiet bug: a base class method that calls a method you replaced.
What this helps you reason about
- Predict which class’s method runs for a call, from the object and not from the declared type.
- Follow a super call and say what this is inside it.
- Spot a count or total that changes twice because a base method calls your method.
An office pays back what its staff spend at work: travel, meals, and hotel nights. Each kind of expense claim has its own rule, such as a meal paid up to 30. The payment code asks a claim for its payout, and one kind of claim is often built from another. This lesson follows how TypeScript finds the method for each call, and what a class that extends another class gets from it.
Already know this? The short version
- TypeScript never chooses a method when it compiles: every call obj.m() looks for m on the object’s class while the program runs, then on each class it extends; the declared type plays no part.
- class B extends A gives B every method of A; a method of the same name in B replaces A’s for B objects, and super.m() runs A’s with the same this.
- A base method that calls this.m() runs the subclass’s m: a CountingBox that counts in add and in addAll counts twice (the fragile base class).
- Composition holds another object and passes calls on to it; the held object then calls its own methods, never yours.
Words in this lesson (6)
- dynamic dispatch
- Choosing which implementation of a method runs while the program runs, from the value the method is called on.
- prototype
- An object that another object inherits properties from. Every plain object inherits from Object.prototype, so it already has properties such as constructor and toString.
- subclass
- A class declared with extends, such as class MealClaim extends Claim. Its objects have the methods of the base class Claim, unless the subclass writes its own version.
- super
- Inside a method, super.payout() runs the base class’s payout. this stays the same object.
- fragile base class
- A base class whose methods call other methods that a subclass may replace. A change in the base class, or an override in the subclass, can then change results in a way neither class shows.
- composition
- Reusing a type by holding a value of it in a field and passing chosen calls on to that value, instead of inheriting from the type.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / Who chooses
Who chooses the method: the compiler or the running program?
When code calls c.payout(), something must decide which payout runs. In some languages the compiler decides, from the declared types in the program. This is static dispatch. In others the running program decides, from the value. This is dynamic dispatch.
TypeScript always uses the second. Types are removed when TypeScript is compiled to JavaScript (Interfaces, traits, and type classes showed that an interface leaves nothing behind). So the declared type of c cannot choose anything. Below, c is declared as Claim but holds a MealClaim, and the MealClaim’s payout runs.
class Claim {
constructor(public amount: number) {}
payout(): number {
return this.amount;
}
}
class MealClaim extends Claim {
payout(): number {
return this.amount > 30 ? 30 : this.amount;
}
}
const c: Claim = new MealClaim(45);
console.log(c.payout()); // 30: MealClaim's payout In TypeScript the object decides which method runs; the declared type never does.
Coming from Java: TypeScript has no overloads chosen by the compiler: one name in a class is one method.
Precisely: overload signatures
TypeScript can declare several signatures for one function, called overloads. They only tell the checker which argument types are allowed. There is still one function body, and it must test the arguments itself.
02 / Finding it
How does a running program find the method?
Each object is linked to its class’s prototype, an object that holds the class’s methods. That prototype is linked to the prototype of the class it extends, and so on. This line of links is the prototype chain.
A call c.line() looks for line on the object, then along the chain, and runs the first one it finds. Inside line, this is still the MealClaim. So this.payout() starts a new lookup at MealClaim, not at Claim, and finds MealClaim’s payout. Where the method was found does not matter; the object does.
class Claim {
// ...
line(): string {
return "paid " + this.payout();
}
}
const c: Claim = new MealClaim(45);
console.log(c.line()); // paid 30 A call looks for the method on the object’s own class first, then on each class above it.
Experiment
Follow a method lookup up the prototype chain
c.line() is not on MealClaim, so the lookup goes on to Claim and finds it there. Inside line, this is still the MealClaim object, so this.payout() starts its lookup at MealClaim again and finds MealClaim’s payout. Without it, the lookup goes on to Claim.
Why it matters Each call looks for the method on the object’s own class first, then on each class above it. The declared type Claim plays no part.
Predict the line. Then delete the payout method from class MealClaim (three lines), and predict again.
class Claim {
constructor(public amount: number) {}
payout(): number {
return this.amount;
}
line(): string {
return "paid " + this.payout();
}
}
class MealClaim extends Claim {
payout(): number {
return this.amount > 30 ? 30 : this.amount;
}
}
const c: Claim = new MealClaim(45);
console.log(c.line());
What to look for
new MealClaim(45) first stops at line 11, in MealClaim, which has no constructor of its own, and then in Claim’s constructor. The stepper names an extra frame <instance_members_initializer> there; it belongs to creating the object. Back at the top level, c shows {amount: 45}: the panel does not show the class. Step into c.line(): the frame line is at line 7, inside class Claim. Step into this.payout(): the frame payout is at line 13, inside class MealClaim. The run prints paid 30. After the edit, the same step enters payout at line 4, inside Claim, and the run prints paid 45.
Expected output
paid 3003 / Inheritance
What does a subclass get from its base class?
class MealClaim extends Claim makes MealClaim a subclass of Claim. A MealClaim object has every method of Claim. A method with the same name in MealClaim overrides Claim’s: it replaces it for MealClaim objects.
The override can still use the base version: super.payout() runs Claim’s payout, with this still the same object. A subclass with a constructor must call super(…) before it uses this.
The override keyword asks the compiler to check that a method really replaces one. A misspelled override payot() reports This member cannot have an 'override' modifier because it is not declared in the base class 'Claim'. Did you mean 'payout'? Without the keyword, payot is a new method, and Claim’s payout stays in use.
class HotelClaim extends Claim {
constructor(amount: number, public nights: number) {
super(amount);
}
override payout(): number {
return super.payout() + 10 * this.nights;
}
} A subclass gets every method of its base class, and replaces the ones it writes again; super.m() runs the base version on the same object.
Coming from Java: override is TypeScript’s @Override: optional, but it turns a misspelled name into a compile error.
04 / Calls inside
Which version runs when an inherited method calls another method?
A base class method can call other methods on this. Those calls are looked up on the object, so they run the subclass’s overrides. That is useful, and it is also a trap.
Below, ClaimBox.addAll calls this.add for each amount. CountingBox counts in add and in addAll. A batch is counted in addAll, then each amount is counted again in add: two claims count as 4. Nothing in CountingBox shows that addAll calls add. This is called the fragile base class problem: the subclass depends on how the base class is written inside.
The fix is to count in one place: in add only, because every way of adding goes through it. That is safe only while ClaimBox.addAll keeps calling add.
class CountingBox extends ClaimBox {
count = 0;
add(amount: number): void {
this.count++;
super.add(amount);
}
addAll(amounts: number[]): void {
this.count += amounts.length; // counts again in add
super.addAll(amounts);
}
} A base method that calls this.m() runs your override of m; a subclass must know which base methods call which.
Experiment
Count claims in a subclass
ClaimBox.addAll calls add for each amount. That call is looked up on the object, a CountingBox, so it runs CountingBox’s add, which counts again. CountingBox counted each claim once in addAll and once in add.
Why it matters A subclass depends on which of its base class’s methods call each other. That is not visible in the subclass, and it can change when the base class changes.
Predict the count. Then delete the line this.count += amounts.length; and predict again.
class ClaimBox {
total = 0;
add(amount: number): void {
this.total += amount;
}
addAll(amounts: number[]): void {
for (const a of amounts) {
this.add(a);
}
}
}
class CountingBox extends ClaimBox {
count = 0;
add(amount: number): void {
this.count++;
super.add(amount);
}
addAll(amounts: number[]): void {
this.count += amounts.length;
super.addAll(amounts);
}
}
const box = new CountingBox();
box.addAll([12, 30]);
console.log(box.count + " claims, total " + box.total);
What to look for
Step into box.addAll(…): the frame addAll is at line 20, in CountingBox, and box shows count: 2 after it. Step into super.addAll: a second frame named addAll appears, at line 7, in ClaimBox, where a shows not initialized yet until the loop starts. Step into this.add(a): the frame list shows add at line 16, in CountingBox, above both addAll frames. The frames name only methods, so the line tells you the class. A line can be stopped on twice in a row; that is how Node reports it. The run prints 4 claims, total 42. After the edit, the run prints 2 claims, total 42.
Expected output
4 claims, total 4205 / Composition
How do you reuse code by holding another value?
Composition reuses a class without extending it. The new class holds an object of the old one in a field and passes calls on to it. This passing on is also called delegation.
Below, CountingBox holds a ClaimBox. Its addAll counts the batch and passes it on. The held box’s addAll then calls the held box’s own add, not CountingBox’s, so nothing is counted twice. The cost: CountingBox must write each method it wants to offer, and it is no longer a ClaimBox. A function that takes a ClaimBox does not accept it, unless both fit a shared interface.
class CountingBox {
count = 0;
constructor(private box: ClaimBox) {}
add(amount: number): void {
this.count++;
this.box.add(amount);
}
addAll(amounts: number[]): void {
this.count += amounts.length;
this.box.addAll(amounts); // the box's own add runs
}
} Composition passes chosen calls on to a held object; the held object then calls only its own methods.
Coming from Go: This is what Go does with a struct field; Go’s embedding writes the passing on for you.
06 / Try it
Can you explain a fresh example?
Decide which limit each call uses.
class Claim {
constructor(public amount: number) {}
limit(): number {
return 50;
}
payout(): number {
return this.amount > this.limit() ? this.limit() : this.amount;
}
}
class TaxiClaim extends Claim {
limit(): number {
return 80;
}
line(): string {
return "taxi " + super.payout();
}
}
const t = new TaxiClaim(70);
console.log(t.payout());
console.log(t.line());
What is printed?
Reveal the reasoning
70, then taxi 70. t.payout() finds payout on Claim, but this is the TaxiClaim, so this.limit() runs TaxiClaim’s limit, 80, and 70 is below it. super.payout() also runs Claim’s payout with the same this, so the limit is 80 there too. super chooses where the lookup for payout starts, not what this is.
Practice tickets
- DISPATCH-101 · Standing desks are billed at the plain desk price → Free
- DISPATCH-102 · Single scanned pages are counted twice → Sign in · Pro
- DISPATCH-103 · Visitors can cancel studio bookings from the door screen → Sign in · Pro