Comp 110 Subclassing

Subclasses and super

When making a class, we can have it extend another class and become a subclass of the class it is extending. When a class extends a parent class, it automatically has everything the parent class has, plus whatever else we specify in the subclass we're creating. Changing a subclass does not change the parent class. The parent class can also be called the super class.

To make a subclass, all we have to do is start our class definition like normal, then after we say the class name, add the extends keyword then the name of the class we're extending from. We also have to make sure that the subclass meets the requirements of the parent class. We'll make use of constructors to do this.

In the first line of the constructor in the subclass, we make a call to the constructor of the super class by writing super(); Inside the parentheses we'd put any arguments required by the constructor of the parent class. super(); is calling the constructor given in the parent class, so the arguments we provide must match in type, order, and number with the parameters given in the constructor of the parent class. It is important to note that this call to the super constructor must be the very first thing to happen in the constructor of the subclass.

Since subclass extends parent class, an object of type subclass is also of type parent class but an object of type parent class is not necessarily of type subclass. Additionally, one super class can have many subclasses that extend it, but each class is only allowed to extend from one parent class. 

Example

Sometimes we might want to get a little more specific when we're making classes, so that's when we can make classes that extend other classes!

For example, we know that all UNC students are students but not all students are UNC students (there are students that go to other schools). We could have a Student class (all students would fit with this) and a UNCStudent class (only UNC students fit with this).

We can make a new UNCStudent object, but UNCStudents are also just Students, so we can translate this idea into our code by making the UNCStudent class extend the Student class.

We could also make our Student class extend a Person super class. We could make a Professor class that extends the Person class if we wanted to. Note that our UNCStudent class still only extends Student-each class is only allowed to extend one super class, but that super class is allowed to extend a different class! Even though we have UNCStudent extends  Student, which extends  Person, we don't say that UNCStudent extends Person. Let's see how this looks:

class Person {
    name: string;
    age: number;
 
    constructor(n: string, a: number) {
        this.name = n;
        this.age = a;
    }
 
    hasBirthday = (): void => {
        this.age++;
    } 
}
 
class Student extends Person {
    year: string = "";
    constructor(name: string, age: number, year: string) {
        super(name, age);
        this.year = year;
    }
}
 
class UNCStudent extends Student {
    onyen: string = "";
 
    constructor(name: string, age: number, year: string, onyen?: string) {
        super(name, age, year);
        if (onyen !== undefined) {
            this.onyen = onyen;
        }
    }
 
    cheer = (): string => {
        return "Go heels!";
    }
}

Here, we've got three classes. The constructor of the Person class takes in a string and a number, so when the Student class extends Person, it must provide a string and a number to satisfy the requirements of the parent class. This happens in the call to super within the constructor of the Student subclass.

Similarly, UNCStudent extends Student, so within the constructor of the UNCStudent class, we must first call super with whatever arguments are required by the constructor of the super class, which is the Student class. That's why we have super(name, age, year); Since Student's constructor requires a string, number, and string, we ask for these values in the constructor of UNCStudent then pass them into the call to the super.

We also see that the cheer method is defined within UNCStudent, so only objects created as UNCStudent objects have this method. Objects created as objects of the Student or Person classes do not have this method since it was defined in the subclass.

Another Example

class Weather {
    typeOfWeather: string;
    isExtreme: boolean;
    duration: number;
 
    constructor(t: string, extreme?: boolean) {
        this.typeOfWeather = t;
        if (extreme !== undefined) {
            this.isExtreme = extreme;
        }
    }
 
    weatherReport = (): void => {
        print("today's weather is: " + this.typeOfWeather);
    }
}
 
class Tornado extends Weather {
    severity: number;
 
    constructor(n: number, length: number) {
        super("tornado", true);
        this.severity = n;
        this.duration = length;
    }
}

In this example, class Tornado is a subclass of the Weather class. As a result, we see inside of Tornado's constructor the first line is the call to super with "tornado" passed in as the string and true as the boolean. The call to super is calling the constructor of the parent class, so we see that what's happening in this case is we enter the constructor of Weather using "tornado" as t and true as the optional parameter extreme. Then, this.typeOfWeather is initialized to be "tornado". 

In Weather's constructor, we see extreme?: boolean. This added ? made the extreme parameter optional. 

More on super and Optional Parameters

An important thing about optional parameters is that they must be declared after all required parameters. The other important thing about optional parameters is that once we make something optional, its type changes to its original type OR undefined. So, when we try to use the value of the optional parameter later, we have to check to make sure it's not undefined first (done in the Tornado class above in the if statement that checks extreme !== undefined). Since in this example with Tornado we passed in true as the boolean for the extreme parameter when we called super, we know it's not undefined so we can go ahead and assign true to this.isExtreme. This all happened within our call to the super constructor.

After taking care of everything with the call to super, we keep going where we left off in the subclass constructor. In the Tornado example, this means assigning the number n we took in to be the value of this.severity and the length number we took in to be the value of this.duration. One thing to notice is that duration isn't defined as a property within Tornado, but it's okay since it's defined as a property of Weather, the parent class. This is fine since anything declared as type Tornado (the subclass) retains all the properties and methods of Weather (the parent class).