Search code examples
typescriptgenericstypescript-generics

TS2344: type inferred from property on generic argument "does not satisfy the constraint" even though the type seems to work fine?


I have an inheritance chain similar to this (fictional names - I realize they are not perfect):

interface Vehicle {}
interface Airplane extends Vehicle {
  number: number;
}
interface Boat extends Vehicle {
  color: string;
}
interface Workshop<TVehicle extends Vehicle = Vehicle> {
  vehicleType: TVehicle;
  repair: (vehicle: TVehicle) => WorkOrder<this>;
}
interface WorkOrder<TWorkshop extends Workshop<TWorkshop["vehicleType"]>> {
  vehicle: TWorkshop["vehicleType"];
}

The idea is that the type of a WorkOrder.vehicle should be inferrable based on the Workshop provided as a type argument:

const orderA: WorkOrder<Workshop<Airplane>> = {
  vehicle: {
    number: 123,
  },
};

const orderB: WorkOrder<Workshop<Boat>> = {
  vehicle: {
    color: "blue",
  },
};

This actually works! I receive editor completion for the above examples and the following produces an error:

const orderA: WorkOrder<Workshop<Airplane>> = {
  vehicle: {
    color: "blue", // ERROR! Object literal may only specify known properties, and 'color' does not exist in type 'Airplane'
  },
};

BUT - I get an error on the type argument TWorkshop["vehicleType"] inside the generic argument for WorkOrder:

Type 'TWorkshop["vehicleType"]' does not satisfy the constraint 'Vehicle'.ts(2344)

I don't understand why, nor why the constraint actually seems to work given the complaint - can anyone explain what is going on?


Edit: Link to TS Playground


Solution

  • This looks like the same issue reported in microsoft/TypeScript#49490, caused by a change in microsoft/TypeScript#49119 released in TypeScript 4.8 to improve reduction of intersection types. It isn't clear whether this is considered a bug, a design limitation, or working as intended. Your generic constraint is circular, and so you can't necessarily expect such things to work as desired.


    That's the answer to the question as asked. But presumably you want to know how to fix it also.

    You could force TypeScript to see that your type argument meets the Vehicle constraint, by intersecting it with Vehicle:

    interface WorkOrder<TWorkshop extends Workshop<TWorkshop["vehicleType"] & Vehicle>> {
      vehicle: TWorkshop["vehicleType"];
    }
    

    Or you could just use the any type to tell TS not to worry... since the constraint you're enforcing isn't particularly useful (of course TWorkshop extends Workshop<TWorkshop["vehicleType"]>, it's more or less a tautology, so you're not losing much by weakening that to any):

    interface WorkOrder<TWorkshop extends Workshop<any>> {
      vehicle: TWorkshop["vehicleType"];
    }
    

    Indeed I would almost certainly use any in practice here, to avoid circularities and to indicate that I don't really care what's going on there other than it's some kind of Workshop. I wouldn't even think about trying to represent the circular self-constraint unless any had some really negative side effect.

    Playground link to code