'Typescript type inference with mapped type and enums in a function why not works?

I can't figure out, why doesn't work the type inference here (see below in the code).

enum Vehicle {
  Car,
  Bus,
  Plane,
}

interface CarParams {
  carId: string;
}

interface BusParams {
  busId: string;
}

interface PlaneParams {
  planeId: string;
}

type Params = {
  [Vehicle.Bus]: BusParams;
  [Vehicle.Car]: CarParams;
  [Vehicle.Plane]: PlaneParams;
};

function showDriver<T extends Vehicle>(vehicle: T, params: Params[T] ): void {
// ...
  if (vehicle === Vehicle.Bus) {
    params.busId //<---- Property 'busId' does not exist on type 'CarParams | BusParams | PlaneParams'.
    // Type inference doesn't work here!
  }
}

showDriver(Vehicle.Bus, { busId: '' }) // <--- type inference works here!


Solution 1:[1]

There is an in operator narrowing available, and we can take advantage of that to identify a particular member type in the union.

enum Vehicle {
  Car,
  Bus,
  Plane,
}

interface CarParams {
  carId: string;
}

interface BusParams {
  busId: string;
}

interface PlaneParams {
  planeId: string;
}

type Params = {
  [Vehicle.Bus]: BusParams;
  [Vehicle.Car]: CarParams;
  [Vehicle.Plane]: PlaneParams;
};

function showDriver<T extends Vehicle>(vehicle: T, params: Params[T]): void {
  // ...
  if ("busId" in params) {
    console.log(params.busId);
  }
  if ("carId" in params) {
    console.log(params.carId);
  }
  if ("planeId" in params) {
    console.log(params.planeId);
  }
}

showDriver(Vehicle.Bus, { busId: 'bus123' });
showDriver(Vehicle.Car, { carId: 'car123' });
showDriver(Vehicle.Plane, { planeId: 'plane123' });

Illustration

"use strict";
var Vehicle;
(function(Vehicle) {
  Vehicle[Vehicle["Car"] = 0] = "Car";
  Vehicle[Vehicle["Bus"] = 1] = "Bus";
  Vehicle[Vehicle["Plane"] = 2] = "Plane";
})(Vehicle || (Vehicle = {}));

function showDriver(vehicle, params) {
  // ...
  if ("busId" in params) {
    console.log(params.busId);
  }
  if ("carId" in params) {
    console.log(params.carId);
  }
  if ("planeId" in params) {
    console.log(params.planeId);
  }
}

showDriver(Vehicle.Bus, {
  busId: 'bus123'
});

showDriver(Vehicle.Car, {
  carId: 'car123'
});

showDriver(Vehicle.Plane, {
  planeId: 'plane123'
});

WYSIWYG => WHAT YOU SHOW IS WHAT YOU GET

Solution 2:[2]

Not entirely sure why it doesn't work, but I believe it might be because the Params type isn't a JS object, and TS doesn't support mapping types like this. I have found other subtle limitations with it, that have made me restructure my code unfortunately. TS is very powerful, but it's still under development and incomplete.

In any case, I took a slightly different approach here seems to work as you intended, and allows you to compare against the enum directly, rather than having to use fields that are unique to each type.

enum Vehicle {
  Car,
  Bus,
  Plane
}

interface CarParams {
  kind: Vehicle.Car;
  carId: string;
}

interface BusParams {
  kind: Vehicle.Bus;
  busId: string;
}

interface PlaneParams {
  kind: Vehicle.Plane;
  planeId: string;
}

type Params = CarParams | BusParams | PlaneParams;

function showDriver(params: Params): void {
  // ...
  if (params.kind === Vehicle.Bus) {
    params.busId = "1"; // exists
    params.planeId = "2"; // does not exist
  }
}

showDriver({
  kind: Vehicle.Bus,
  busId: "", // exists
  planeId: "" // does not exist
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Nalin Ranjan
Solution 2