Reputation: 76
I have a non generic singleton class (eg service). It has a set of fields whose type is a descendant of a generic type. I want to implement a generic method on that class which has a key parameter that selects one of the fields. This method however should return the generic ancestor. A contrived example of a rental company and vehicles:
const enum VehicleType {
Car,
Truck,
}
class Vehicle {
constructor(readonly type: VehicleType) {}
}
class Car extends Vehicle {
constructor(readonly numberOfPassengers: number) {
super(VehicleType.Car);
}
}
class Truck extends Vehicle {
constructor(readonly isPickup: boolean) {
super(VehicleType.Truck);
}
}
class VehicleList<T extends Vehicle> {
readonly availableArray = new Array<T>();
constructor(readonly type: VehicleType) {}
}
class CarList extends VehicleList<Car> {
smallCarAvailabilityLow: boolean = false;
constructor() {
super(VehicleType.Car);
}
}
class TruckList extends VehicleList<Truck> {
pickupCount: number = 0;
constructor() {
super(VehicleType.Truck);
}
}
class RentalCompanyAssets {
readonly cars = new CarList();
readonly trucks = new TruckList();
// Does not work as cannot match type to generic parameter
getGenericVehicleListOfType<T extends Vehicle>(type: VehicleType): VehicleList<T> {
return type === VehicleType.Car ? this.cars : this.trucks;
}
// Works but needs to be cast to generic type in calling function (see below)
getVehicleListOfType(type: VehicleType): CarList | TruckList {
return type === VehicleType.Car ? this.cars : this.trucks;
}
// Variation based on comments below
// However get error: 'T' could be instantiated with a different subtype of constraint 'Vehicle'
getGenericVehicleListOfType2<T extends Vehicle, K extends VehicleType>(type: K): VehicleList<T> {
return type === VehicleType.Car ? this.cars : this.trucks;
}
// Variation based on comments below
// However get error: 'T' could be instantiated with a different subtype of constraint 'Vehicle'
getGenericVehicleListOfType2Old<T extends Vehicle, K extends VehicleType>(type: K): VehicleList<T> {
const v: { [P in VehicleType]: VehicleList<T> } = {
[VehicleType.Car]: this.cars,
[VehicleType.Truck]: this.trucks
}
return v[type];
}
}
I (probably) can get this to work by using non generic getVehicleListOfType
but casting the return value in the calling function:
// Handles allocation behaviour common to cars and trucks
abstract class VehicleTypeAllocator<T extends Vehicle> {
protected readonly _vehicleList: VehicleList<T>;
constructor(
readonly type: VehicleType,
readonly assets: RentalCompanyAssets,
) {
this._vehicleList = this.getVehicleList();
}
getVehicleList(): VehicleList<T> {
return this.assets.getGenericVehicleListOfType(this.type) as unknown as VehicleList<T>;
}
abstract specialProcessingAfterAllocation(): void;
}
// Handles special allocation behaviour for cars
class CarAllocator extends VehicleTypeAllocator<Car> {
declare readonly _vehicleList: CarList;
constructor(assets: RentalCompanyAssets) {
super(VehicleType.Car, assets);
}
override specialProcessingAfterAllocation() {
if (this._vehicleList.smallCarAvailabilityLow) {
this.sendAlert();
}
}
}
How can I do this properly without having to cast?
Upvotes: 1
Views: 49
Reputation: 981
The generic type T
might have type of Car
or Truck
, but it also might have very different type.
For example, you could declare
class Bus extends Vehicle {
}
and pass it as generic parameter. In such case, return value for the function will be incompatible with expected function result.
Casting is not a way out, because by doing so, your function return type might just not match an actual result.
There are different ways to handle this.
You might use type union as you did in Works example.
Another similar way to handle this is to return conditional type as a result:
getGenericVehicleListOfType<T extends Vehicle>(type: VehicleType): T extends Car ? VehicleList<Car> : VehicleList<Truck> {
...
}
But I would advise you against it and stick with type union for your solution.
Upvotes: 0