Alex
Alex

Reputation: 3855

How to redeclare parent method in a subclass?

I have an abstract Vehicle class that has abstract Wheels. I know that all subclasses of Vehicle will compute their _wheels in the same way, so to avoid duplication, I write the implementation in the base class:

abstract class Vehicle {
  abstract _wheels: Wheel[];
  wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

abstract class Wheel {}

Then I create a specific subclass:

class Car extends Vehicle {
  _wheels: CarWheel[];
  constructor() {
    super();
    this._wheels = [];
  }
}

class CarWheel extends Wheel {
  hubcap = new CarWheelHubcap()
}
class CarWheelHubcap {}

It doesn't have Wheels but CarWheels with a CarWheelHubcap. When it's time to enumerate them however, I run into a problem:

const car = new Car();
car.wheels().forEach(wheel => wheel.hubcap);
                              ~~~~~~~~~~~~

Error: Property 'hubcap' does not exist on type 'Wheel'.

I would expect this not to happen since we've already re-declared _wheels as CarWheel[]. My guess is it goes up the prototype chain, finds Vehicle.prototype.wheels and concludes its return type is Wheel (?)

Question

Is it possible to fix this without re-implementing wheels() method in Car class? Once again, the subclasses will all share Vehicle.prototype.wheels so it'd be wasteful to re-write

wheels() {
  return super.wheels() as CarWheel[];
}

in each subclass.

Follow-up question

Modified playground link

Upvotes: 1

Views: 208

Answers (1)

Aluan Haddad
Aluan Haddad

Reputation: 31863

Given your sample code,

class CarWheelHubcap { }

class CarWheel extends Wheel {
  hubcap = new CarWheelHubcap();
}

and continuing on to where you encounter the error,

const car = new Car();
car.wheels().forEach(wheel => wheel.hubcap);

Error: Property 'hubcap' does not exist on type 'Wheel'.

As the error says, Wheel does not define a member named hubcap.

Your approach of trying to redeclare, that is to say shadow, the _wheels property in Car with a more specific type has numerous problems. For one, it is not type safe because arrays are mutable. For another, the actual error stems from the return type of the wheels() method being inferred from the type of the property _wheels in the Vehicle class, leaving you in a pickle of writing extra, pointless code or writing complex computed type annotations that aren't necessary for this relatively simple situation.

Instead we will leverage Parametric Polymorphism. This concept is exposed in TypeScript as Generics and you can read about them in the TypeScript: Handbook - Generics section.

It seems that you want to express that Car in inheriting from Vehicle.prototype, indeed has wheels, but that they are not merely Wheels but CarWheels. Inheritance isn't sufficient to express such a biaxial relationship, we need to use generics to express and enforce that relationship.

Let's take an initial stab:

class Vehicle<TWheel extends Wheel> {
  abstract _wheels: TWheel[];
  wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

What we've done is parameterize Vehicle with a type parameter, TWheel that specifies what wheels it will have. Furthermore, we have stipulated that any type argument provided for the type parameter TWheel must extend Wheel.

Now in defining Car, we must pass a type argument for TWheel

class Car extends Vehicle<CarWheel> {
  _wheels: CarWheel[] = [];
}

Note: I removed the redundant empty constructor, replacing it with a field initializer. This is equivalent to constructor() {super(); this._wheels = [];} while being easier to read and easier to maintain.

However, since the type of the elements of the _wheels array is passed to Vehicle<TWheel> when we extend it, we can improve the code further by writing removing the abstract modifier from _wheels in vehicle, implementing the array there and removing any need to define it in subclasses.

class Vehicle<TWheel extends Wheel> {
  _wheels: TWheel[] = [];
  wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

And this simplifies Car to

class Car extends Vehicle<CarWheel>{ }

With Car parameterizing Vehicle with CarWheel, your original code now works.

car.wheels().forEach(wheel => wheel.hubcap);

The above now works because a Car's wheels are known to be CarWheel

Note: To maintain compatibility with existing code, and to allow vehicles that don't have specialized wheels to extend vehicle simply and concisely, we can specify a default for our type parameter. Thus, if say another class, say representing armored vehicles, doesn't use specialized wheels, we can just write class Armor extends Vehicle { }.

class Vehicle<TWheel extends Wheel = Wheel> {
  _wheels: TWheel[] = [];
  wheels() {
    return this._wheels;
  }
}

Finally, let's touch up the Vehicle to make it more idiomatic. wheels doesn't need to be a method, we can use a get property accessor.

class Vehicle<TWheel extends Wheel = Wheel> {
  get wheels() { return this._wheels; }
}

This let's consumers write car.wheels.forEach(...) without thinking about the encapsulation technique ("oh this is a wrapper method") something they needn't worry their heads about.

Putting it together:

class Wheel {
  diameter = 30;
}

class Vehicle<TWheel extends Wheel = Wheel> {
  _wheels: TWheel[] = [];
  get wheels() {
    return this._wheels; // assume it's more complex than this
  }
}

class CarWheelHubcap {
  size = 15;
}

class CarWheel extends Wheel {
  hubcap = new CarWheelHubcap();
}

class Car extends Vehicle<CarWheel> {
  drivetrain: 'AWD' | 'FWD' | 'RWD' = 'AWD';
}

const car = new Car();

car.wheels
  .map(wheel => wheel.hubcap)
  .forEach(console.log);

Playground Link

Note that I've added a member to each class because empty classes are a poor practice that can lead to bugs and cause confusion and mayhem.

Also, if we need a constructor that takes an array of wheels, parametric polymorphism solution continues to work well for use. We can simply add a constructor to Vehicle that takes the wheels, and all subclasses will expose an appropriately typed constructor requiring an array of the specified TWheel. Playground Link

Upvotes: 1

Related Questions