I have gone down the "JavaScript can’t do multiple inheritance, but…" rabbit-hole and this is where I have ended up. I have got some basic functionality going, but a large part of why I am using classes is to enforce some basic validation and there are some key aspects I just can’t get working despite a lot of searching. I have done up some example code below – in this example I have a Car class and an Aircraft class and I want to combine the both into a FlyingCar class.
class Car {
#color
constructor() {
Object.defineProperty(this, "Color", {
configurable: true,
enumerable: true,
get: function () {
return this.#color
},
set: function (value) {
if (value && typeof value !== 'string') {
return console.error('Error: Color must be a string')
}
this.#color = value
}
})
}
}
class Aircraft {
#wingspan
constructor() {
Object.defineProperty(this, "Wingspan", {
configurable: true,
enumerable: true,
get: function () {
return this.#wingspan
},
set: function (value) {
if (value && typeof value !== 'number') {
return console.error('Error: Wingspan must be a number')
}
this.#wingspan = value
}
})
}
static fly() {
console.log('whhhhheeeee! I am flying!')
}
}
class FlyingCar extends Car {
constructor() {
super()
Object.assign(this, Aircraft) // using Aircraft as a kind of mixin
// Object.preventExtensions(this) // can’t do this as it prevents me from accessing Aircraft properties
}
}
// put it all together
const flyingCar = new FlyingCar()
flyingCar.Color = 42 // returns an error as expected :-)
flyingCar.Wingspan = 'green' // should fail as wingspan is not a number. But is allowed :(
FlyingCar.fly() // doesn't work. Seems like static properties are not carried through with Object.assign :(
flyingCar.FavoriteIceCream = 'chocolate' // should fail with object is not extensible, but is allowed :(
The code at the bottom shows what isn’t working but to pull this out:
- I can copy properties across from Aircraft using
Object.assign
but I can’t seem to copy across the property definitions Object.assign
doesn’t copy across static methods- I can’t close off the class using
Object.preventExtensions
as this prevents me from assigning properties belonging to the Aircraft class
I’d like to have a class-based solution, but would consider a factory (a function that returns a new FlyingCar). I haven’t tried proxies as they just seem overly complex but maybe they could be useful.
I am after a JavaScript-only solution, no TypeScript.
2
Answers
If you want to go the route of mixins, it could look like this. But the problem of using
Object.assign
is that you assign properties to the FlyingCar class outside of the class itself, which is not very OOP.I’d strongly recommend using composition, not inheritance, to create your
FlyingCar
class. JavaScript simply does not have multiple inheritance, and attempts to fake it are doomed to fail in various ways, sometimes subtle ways.But let’s first look at why your current implementation doesn’t work, and what we could do to make it work following that approach.
Object.assign
copies the current value of an accessor, not the accessor itself, as though you’d donetarget.prop = source.prop
. You can see that here:So we can’t just use
Object.assign
for this.Separately, to use
Aircraft
instance properties and fields (bothWingspan
and#wingspan
are instance-specific), you’ll need an instance ofAircraft
— those don’t exist on theAircraft
constructor function or onAircraft.prototype
. We can solve that by having a private field on theFlyingCar
instance. Then we can create instance properties for theAircraft
properties and methods and delegate to thatAircraft
instance. Similarly, to get thefly
static method, we can delegateAircraft
properties and methods toAircraft
fromFlyingCar
.That could look something like this (note that
addFacades
is just a sketch, not an all-singing, all-dancing implementation [for instance, it doesn’t try to handle inherited properties]):Note that in order to get an actual error from assigning to
flyingCar.FavoriteIceCream
, you need this code to be in strict mode. (Maybe yours is, if it’s in a module. In the above, since it’s not a module, I needed the directive.)At this point,
FlyingCar
inherits fromCar
and mixes inAircraft
. That is, it’s half inheritance and half composition. That might be reasonable if the class is "mostly" aCar
and only partially anAircraft
, but it’s still…assymmetrical. That lack of balance suggests it’s not the best approach.Which brings us back to: I’d use composition, not inheritance, and I’d probably lean toward doing it manually rather than using
addFacades
above. Perhaps something like this:(In the above, I’ve also used accessor definitions rather than
Object.defineProperty
to create the accessors for#color
and#wingspan
, and used standard JavaScript naming conventions [color
, rather thanColor
, and the same forwingspan
/Wingspan
].)But if you wanted to do it more automatically, look for the various mixin helpers that people have built, or build your own (perhaps starting from
addFacades
).