最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

TypeScript: Get field value using generic method from non generic class without cast - Stack Overflow

programmeradmin5浏览0评论

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?

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?

Share Improve this question edited Jan 22 at 0:24 Paul Klink asked Jan 18 at 4:35 Paul KlinkPaul Klink 764 bronze badges 7
  • 1 I'm running into a bunch of issues with your code before I even get to your question. You've got empty classes which make for poor examples, especially with unused generics. You should add structure that distinguishes the types. And you've got classes with uninitialized properties, and you never new them, so why not interfaces instead? Please edit to clear this stuff up so we can focus on your issue. – jcalz Commented Jan 18 at 15:02
  • 1 (see prev comment). Like, if I try to fix these problems and then work on your issue, I get the code at this playground link, using the techniques in ms/TS#47109 to do this sort of case analysis. But obviously that is different enough from the code in your question to not be directly applicable as written. So please do edit when you get a chance to make sure that there are no empty/indistinguishable classes, and that all generic types depend structurally on their type arguments. – jcalz Commented Jan 18 at 15:18
  • I have edited as per @jcalz comments. I have left the example with classes as that is what I am using in my actual code. The playground link was helpful as I now understand that the mapping interface is dynamically generated in method but does not generate any JavaScript code (becomes easier in TS5.8) Base on the playground code, I have added new variations to the methods in example. playground However CarList and VehicleList now have different shapes and I am getting the error shown in the example. – Paul Klink Commented Jan 21 at 20:44
  • I am still confused why your example attempts have no link between the input and output generics. Either you have an input of nongeneric VehicleType or of generic type K extends VehicleType but your output is VehicleList<T>. How would T have any relationship with the input? Where do you expect T to be set? I'd imagine if you use K, then the output type should depend on K like this. No T. There are various ways to proceed but I'm still not sure why you're expecting your code to do what you want... could you articulate why T is there? – jcalz Commented Jan 21 at 21:18
  • VehicleTypeAllocator is generic and provides the input T. It implements common behaviour and descendants (eg CarAllocator) handle specific behaviour for each type. I have updated the example (VehicleAllocator) to better show this. Also Playground link. – Paul Klink Commented Jan 22 at 0:34
 |  Show 2 more comments

1 Answer 1

Reset to default 0

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.

发布评论

评论列表(0)

  1. 暂无评论