Problem overview
A shop ships parcels in three ways. Shipping is the standard option: name() returns "Standard" and cost(weight) returns 5 + weight. Express and Economy extend Shipping and replace both methods with their own versions. Express charges 10 + 2 * weight. Economy charges 3 plus half the weight, rounded down.
For a 4 kilogram parcel, the three options answer like this:
Standard: 9
Express: 18
Economy: 5
Approach: one parent, two overrides
All three options answer the same two questions, so they share one shape: the parent class defines name and cost, and each child class declares methods with the same names and parameters. A method in a child with the same name as one in the parent overrides it. When you call name() on an Express object, the Express version runs, even if the variable holding the object is typed as Shipping.
That is what makes the calling code simple. It can hold a list of Shipping values, call name() and cost(weight) on each one, and get the right answer for every kind, with no if on the option's kind.
Step by step
- In
Shipping, makenamereturn"Standard". - Make
costreturn5 + weight: the base price plus 1 per kilogram. - In
Express, write anamewith the same header as the parent's. It returns"Express". - Write
Express's owncost:10 + 2 * weight. Without it, an express parcel would get the parent's price. - In
Economy, overridenameto return"Economy". Each class overrides only for itself, so nothing here changesExpressorShipping. - Override
costwith3 + Math.floor(weight / 2).
The solution in TypeScript
class Shipping {
name(): string {
return "Standard";
}
cost(weight: number): number {
return 5 + weight;
}
}
class Express extends Shipping {
name(): string {
return "Express";
}
cost(weight: number): number {
return 10 + 2 * weight;
}
}
class Economy extends Shipping {
name(): string {
return "Economy";
}
cost(weight: number): number {
return 3 + Math.floor(weight / 2);
}
}
TypeScript details worth knowing:
/keeps the fraction. In JavaScript and TypeScript every number is a double, so7 / 2is3.5.Math.floorrounds it down to3. For these weights (whole, non-negative numbers) that is the "round down" the lesson asks for.- No constructors needed. The classes have no fields, so the default constructor is enough, and
new Express()works as it is. - Overriding is by name. A child's method must be compatible with the parent's: same name, and a parameter and return type that fit. TypeScript doesn't need an
overridekeyword here. - Types vanish at run time. The annotation
Shippingon a variable only tells the compiler what it may call. The JavaScript that runs looks the method up on the actual object, which is why the child's version wins.
Seeing the override work
Put the three objects in an array typed Shipping[] and ask each one the same questions:
const options: Shipping[] = [new Shipping(), new Express(), new Economy()];
for (const o of options) console.log(o.name() + ": " + o.cost(4));
for (const o of options) console.log(o.name() + ": " + o.cost(0));
console.log(new Economy().cost(7), new Economy().cost(1));
This prints:
Standard: 9
Express: 18
Economy: 5
Standard: 5
Express: 10
Economy: 3
6 3
With weight 0 only the base prices are left. For Economy, weight 7 gives 3 + 3 = 6 and weight 1 gives 3 + 0 = 3.
Why the child's version runs
A TypeScript class compiles to a JavaScript class, and extends links the child's prototype to the parent's. When you call x.name(), JavaScript looks for name on the object's own class first and walks up to the parent only if it finds nothing. So a method written in Express is found before the one in Shipping, and a method that Express doesn't write is inherited from Shipping.
Declare const x: Shipping = new Express(); and the compiler lets you call only what Shipping has, but the object is still an Express: x.name() returns "Express" and x.cost(1) returns 12, and x instanceof Express is true. This is why the code that tests your classes can treat every option the same way and still get three different answers.
Inheritance also means a child never has to repeat what it shares with the parent. Here the children share only the method names, because both methods are overridden. If a child wrote only name, its cost would come from Shipping unchanged.
Edge cases and common mistakes
- Forgetting to override
cost. A child class that overrides onlynameinherits the parent's price. A classBad extends Shippingwith a workingcostbut nonameanswersname()with"Standard": it printsStandard 18for a weight of 4 when the cost is10 + 2 * w. Each method needs its own override. - Forgetting
Math.floor. Without it,Economy's cost for weight 7 is6.5, and for weight 1 it is3.5. - Wrong spelling in the text. The names are returned as they are, so
"Express"and"express"are different strings. - Changing the parent to fit a child. Leave
Shippingas the standard option. Express and Economy carry their own rules. - The largest weight. At 1000 kilograms,
Economycharges 503 andExpresscharges 2010. Plain numbers hold these easily, so there is nothing to guard against, as long as each formula is written exactly. - Copying one child to make another. If you paste
Express's class to startEconomy, change both the name text and the formula. A leftover10 + 2 * weightmakes the two classes agree on the price and the lesson's tests fail on the economy lines. - Weight 0. Valid input: it leaves just the base prices, 5, 10 and 3.
Testing
- weight 4 gives
Standard: 9,Express: 18,Economy: 5. - weight 0 gives
Standard: 5,Express: 10,Economy: 3. - weight 7 gives
Economy: 6,Standard: 12,Express: 24.
