Start learning free

LESSON 564 OF 573 · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

Shapes Perimeter in TypeScript: Polymorphism with a Base Class

Practice O(n) Read the full explanation

A drawing app keeps rectangles, squares and triangles in one list. Write the shape types so that code holding that list can ask any shape for its name and its perimeter (the length all the way around it) without knowing which kind of shape it is. That is polymorphism: one call, shape.perimeter(), and each kind of shape runs its own version.

Every shape class inherits from the class Shape, the common type. Shape has two methods:

  • name() returns the kind of shape as text: "Shape" for a plain Shape.
  • perimeter() returns the length around the shape: 0 for a plain Shape.

Each shape class overrides both:

  • Rectangle(width, height): its name is "Rectangle", its perimeter 2 × (width + height).
  • Square(side): its name is "Square", its perimeter 4 × side.
  • Triangle(a, b, c): its name is "Triangle", its perimeter a + b + c.

In TypeScript a class inherits with class Rectangle extends Shape, and a method with the same name as the parent's replaces it for that class. Each class declares its fields, and its constructor calls super() before it stores anything in this.

Then write two plain functions that get any shapes as a Shape[]:

  • totalPerimeter(shapes) returns the sum of the perimeters of all the shapes.
  • largestShape(shapes) returns the name of the shape with the largest perimeter. If several shapes share the largest perimeter, the one that comes first wins.

Both functions must work through the common type's methods only: never check which kind a shape is. Then a new kind of shape, added later, works with them unchanged.

The starter code already has every class, constructor and method header: write their bodies. For each test, the harness reads the shapes, builds them in the order given, calls totalPerimeter and largestShape, and prints the two results on one line, separated by a space.

 

Example 1:

Input:
    new Rectangle(3, 4)
    new Square(5)
    new Triangle(3, 4, 5)
Output: 46 Square
Explanation: The perimeters are 2 × (3 + 4) = 14, 4 × 5 = 20 and 3 + 4 + 5 = 12. Their total is 46, and the square's 20 is the largest.

Example 2:

Input:
    new Rectangle(2, 4)
    new Square(3)
    new Triangle(4, 4, 4)
Output: 36 Rectangle
Explanation: All three perimeters are 12: 2 × (2 + 4), 4 × 3 and 4 + 4 + 4. On a tie the shape that comes first in the list wins, so the answer is the rectangle.

Example 3:

Input:
    new Square(1)
    new Rectangle(1, 1)
    new Triangle(2, 2, 3)
Output: 15 Triangle
Explanation: The perimeters are 4, 4 and 7, so the last shape, the triangle, is the largest. A 1 by 1 rectangle has the same sides as a square, but it is still a Rectangle: each object answers name() for its own class.

 

Constraints:

  • Every test has between 1 and 100 shapes: the list is never empty.
  • Every size (width, height, side, and each side of a triangle) is an integer from 1 to 104.
  • A triangle's three sides always make a real triangle: each side is shorter than the other two together.

Note:

Each function looks at every shape once: O(n) time for n shapes, and no extra space.

STEP 1 OF 25
    Ctrl + Enter to check

    TYPESCRIPT · OBJECT-ORIENTED PROGRAMMING - CHALLENGES

    Shapes Perimeter in TypeScript, explained

    Problem overview

    A drawing app keeps rectangles, squares and triangles in one list. You write a common type, Shape, and three classes that extend it: Rectangle(width, height), Square(side) and Triangle(a, b, c). Every shape answers two questions: name() and perimeter(). A plain Shape answers "Shape" and 0; each subclass overrides both methods with its own rule:

    • Rectangle: 2 * (width + height);
    • Square: 4 * side;
    • Triangle: a + b + c.

    Then two plain functions take a Shape[]. totalPerimeter(shapes) adds up all the perimeters, and largestShape(shapes) returns the name of the shape with the largest perimeter, the earliest one on a tie. For Rectangle(3, 4), Square(5) and Triangle(3, 4, 5) the perimeters are 14, 20 and 12, so the total is 46 and the largest is "Square".

    Approach: ask the object, don't check its kind

    The two functions never test what kind of shape they hold. They call shape.perimeter() and shape.name() and let each object run its own version. That is polymorphism: one call, many behaviours. The payoff is that a new shape class, such as a circle added later, works with totalPerimeter and largestShape without changing either function.

    Both functions look at each shape once, so each takes O(n) time for n shapes and no extra space beyond a few variables.

    Step by step

    1. The base class. Shape.name() returns "Shape" and Shape.perimeter() returns 0. Every subclass overrides these, so the defaults are never printed. They give the common type its two methods, and that is why the functions can call them on any shape.
    2. Fields. Each subclass declares the numbers it needs as fields: width and height, or side, or a, b and c. Without them the object couldn't remember its sizes until perimeter() is called.
    3. Constructors. A constructor of a subclass calls super() first, which runs Shape's constructor. Only after that can it use this, so the field assignments come next. The parameters share their names with the fields, and this. picks the field.
    4. The overrides. name() returns the class's own name, spelled exactly with its capital letter. perimeter() applies the class's formula to the stored fields.
    5. totalPerimeter. Start a let total = 0, loop with for...of, add shape.perimeter() to total and return it.
    6. largestShape. Start with largest as the first shape, loop over all of them, and replace largest only when a shape's perimeter is strictly greater. Return largest.name().

    The solution in TypeScript

    class Shape {
        name(): string {
            return "Shape";
        }
    
        perimeter(): number {
            return 0;
        }
    }
    
    class Rectangle extends Shape {
        width: number;
        height: number;
    
        constructor(width: number, height: number) {
            super();
            this.width = width;
            this.height = height;
        }
    
        name(): string {
            return "Rectangle";
        }
    
        perimeter(): number {
            return 2 * (this.width + this.height);
        }
    }
    
    class Square extends Shape {
        side: number;
    
        constructor(side: number) {
            super();
            this.side = side;
        }
    
        name(): string {
            return "Square";
        }
    
        perimeter(): number {
            return 4 * this.side;
        }
    }
    
    class Triangle extends Shape {
        a: number;
        b: number;
        c: number;
    
        constructor(a: number, b: number, c: number) {
            super();
            this.a = a;
            this.b = b;
            this.c = c;
        }
    
        name(): string {
            return "Triangle";
        }
    
        perimeter(): number {
            return this.a + this.b + this.c;
        }
    }
    
    function totalPerimeter(shapes: Shape[]): number {
        let total = 0;
        for (const shape of shapes) {
            total += shape.perimeter();
        }
        return total;
    }
    
    function largestShape(shapes: Shape[]): string {
        let largest = shapes[0];
        for (const shape of shapes) {
            if (shape.perimeter() > largest.perimeter()) {
                largest = shape;
            }
        }
        return largest.name();
    }
    

    TypeScript details worth knowing:

    • Field declarations. In this form, a field is declared in the class body (width: number;) and assigned in the constructor. Without the declaration, this.width = width is a compiler error: Property 'width' does not exist on type 'Rectangle'.
    • super() before this. In a derived class, touching this before calling super() is a compiler error: it reports that 'super' must be called before accessing 'this' in the constructor of a derived class.
    • The variable's type. let largest = shapes[0] gives largest the type Shape, so it can later hold a Rectangle, a Square or a Triangle.
    • Types are erased. The parameter type Shape[] only lets the compiler check the calls. At run time each element is the real object, and JavaScript runs the method of its own class.

    Seeing it run

    Build the three lists from the examples and call both functions:

    const a = [new Rectangle(3, 4), new Square(5), new Triangle(3, 4, 5)];
    console.log(totalPerimeter(a), largestShape(a));
    const b = [new Rectangle(2, 4), new Square(3), new Triangle(4, 4, 4)];
    console.log(totalPerimeter(b), largestShape(b));
    const c = [new Square(1), new Rectangle(1, 1), new Triangle(2, 2, 3)];
    console.log(totalPerimeter(c), largestShape(c));
    console.log(new Shape().name(), new Shape().perimeter());
    46 Square
    36 Rectangle
    15 Triangle
    Shape 0

    In the second list all three perimeters are 12, so the first shape, the rectangle, wins the tie. In the third list a 1 by 1 rectangle has the same sides as the square before it, but it is still a Rectangle: each object answers name() for its own class.

    To see that the functions need no changes for a new kind of shape, add a class of your own in a separate file:

    class Circle extends Shape {
        constructor(public r: number) { super(); }
        name() { return "Circle"; }
        perimeter() { return Math.round(2 * 3 * this.r); }
    }
    const d: Shape[] = [new Square(2), new Circle(5)];
    console.log(totalPerimeter(d), largestShape(d));
    38 Circle

    The circle's perimeter here is a rough 30 (a stand-in with 3 for pi), and totalPerimeter adds it to the square's 8 without knowing it is a circle.

    Edge cases and common mistakes

    • A tie. Using >= instead of > lets the later shape replace the earlier one, so example 2 would answer "Triangle" instead of "Rectangle". Only a strictly larger perimeter replaces the current largest.
    • A single shape. The list is never empty, so shapes[0] exists. With one shape the loop compares it with itself and changes nothing.
    • Checking the kind. Writing if (shape instanceof Square) inside the functions works for today's shapes but breaks the idea: every new class would need another branch. Call the methods and stop there.
    • Forgetting super(). A constructor in a derived class that never calls super() is a compile-time error, and one that uses this first is also rejected.
    • Dropping the factor of two. A rectangle has two sides of each size: 2 * (width + height), not width + height.
    • Mixing up the constructor's parameters. A triangle takes three sides in the order a, b, c. The perimeter is a sum, so the order doesn't change the result, but storing the same parameter twice does: check that each of the three fields gets its own value.
    • Name spelling. The names are printed as they are, so "square" and "Square" are different outputs.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free