In TypeScript, how can I get a more specific subclass of a more generic class to allow referencing more specific properties and methods of that subclass when parent classes define them further up the inheritance tree?
To explain my question, I think this code sums up what I’m trying to achieve:
class Driver {} class Formula1Driver extends Driver { useFormula1Skills () { console.log('Go go go!') } } class Car { public driver: Driver constructor(driver: Driver) { this.driver = driver; } } class Formula1Car extends Car { constructor(driver: Formula1Driver) { super(driver) this.driver.useFormula1Skills() // TS Error: Property 'useFormula1Skills' does not exist on type 'Driver'.(2339) } }
Note the type error above.
A Car
must have a Driver
, but a Formula1Car
must have a Formula1Driver
and be able to call Formula1Driver
-specific properties and methods.
I do not want Driver
to define useFormula1Skills
for obvious reasons, and figured that by stipulating that a Formula1Driver
must be passed to the constructor in Formula1Car
, that the type checker would allow my referencing of the subclass-specific method.
Advertisement
Answer
You can narrow down (override) the property in the subclass definition:
class Formula1Car extends Car { constructor(public driver: Formula1Driver) { super(driver) this.driver.useFormula1Skills() } }
It wouldn’t work the other way arround.
Note that above notation is equivalent to:
class Formula1Car extends Car { public driver: Formula1Driver constructor(driver: Formula1Driver) { super(driver) this.driver = driver; ... } }
The reason it works is that Car requires that the driver is Driver and F1Driver indeed is a Driver (note that since Driver does not have any properties, any object can be considered a Driver). When overriding properites, you can safely narrow them down – any kind of Driver can drive a Car, therefore F1Driver is okay to be type of driver in F1Car.
It is a design choice of typescript to be easier to work with but it indeed is supectible to runtime errors like in this case:
const vettel = new Formula1Driver(); const astonMartin = new Formula1Car(vettel); const f1OnTheStreet: Car = astonMartin; // UH OH!!! f1OnTheStreet.driver = new Driver(); // remember that f1OnTheStreet is the same instance of F1 astonMartin.driver.useFormula1Skills(); // astonMartin.driver.useFormula1Skills is not a function
You cannot make a property that you want to access in the subclass private, but you can make it protected, meaning it cannot be accessed from the outside the class but can be access from a subclass. Note that modifiers must match. If you have a private property in Car, you cannot change (access) it in F1Car. If you have public in Car, you cannot make it private in F1Car.
class Car { constructor(protected driver: Driver) {} } class F1Car extends Driver { constructor(protected driver: F1Driver) { super(driver); } }
This still allows Car to implement something like switchDrivers
end end up with the same runtime error as above.
To make sure that nobody changes drivers, the property in Car needs to be readonly (and also in the subclasses
class Car { constructor(public readonly driver: Driver) {} }
Just note that these checks are only compile time so anything can happen if you access them without type checking.