Could TypeScript model this function if it had dependent types?

61 views Asked by At

Say I have a TypeScript type for Vehicles that I'm modeling as the union of two interfaces. I also have a visitor interface for working with vehicles:

interface Car { type: 'car'; make: string; model: string; }
interface Bicycle { type: 'bicycle'; brand: string; numGears: number; }

type Vehicle = Car | Bicycle;

interface VehicleVisitor {
  car: (v: Car) => void;
  bicycle: (v: Bicycle) => void;
}

// or:
// type VehicleVisitor = { [T in Vehicle['type']]: (v: Extract<Vehicle, {type: T}>) => void; };

Now I want to write the function that applies a visitor to a vehicle:

function visitVehicle(vehicle: Vehicle, visitor: VehicleVisitor) {
  visitor[vehicle.type](vehicle);
  //                    ~~~~~~~
  // Argument of type 'Vehicle' is not assignable to parameter of type 'never'.
  //   The intersection 'Car & Bicycle' was reduced to 'never' because property
  //   'type' has conflicting types in some constituents.
}

TypeScript knows that vehicle.type is "car" | "bicycle", so visitor[vehicle.type] is (v: Car) => void | (v: Bicycle) => void. To call that safely, you'd need a value in Car & Bicycle, i.e. never, hence the error.

But the call is safe. You can convince TypeScript of it by spelling things out a little more:

function visitVehicleVerbose(vehicle: Vehicle, visitor: VehicleVisitor) {
  if (vehicle.type === 'car') {
    visitor[vehicle.type](vehicle);  // ok
  } else {
    visitor[vehicle.type](vehicle);  // also ok
  }
}

This works because TypeScript is able to narrow the type of vehicle to Car in the first branch and Bicycle in the second. But the code is exactly the same in both branches, so the if statement isn't necessary.

I've been trying to understand what dependent types are, and this TypeScript issue makes me think that this situation is at least in the ballpark.

Is this the sort of problem that dependent types solve? Or is this something different entirely? Or maybe it's just a gap in TypeScript's type checker that other languages can handle? Or maybe this code isn't correct and there's a case I'm not thinking of.

(In the real code that inspired this, I just wrote vehicle as never to work around this.)

0

There are 0 answers