Gildas Garcia
Gildas Garcia

Reputation: 7066

How to infer correct return type of a function given a type map?

I have a function that, given it knows about the mapping between a key and a type, should return the correct type when called with the matching key.

Below is a simplified example of my use case. I'm almost there but I'm missing something.

Note: this is for a library and we don't know the types as users will provide them.

type Plane = {
    name: string;
    wings: number;
};

type Car = {
    name: string;
    wheels: number;
};

type Vehicles = {
    cars: Car;
    planes: Plane;
};

type TypeMap = { [key: string]: any };

type Mapper<VehicleMapType extends TypeMap = TypeMap> = {
    getVehicle<
        VehicleType extends
            | Extract<keyof VehicleMapType, string>
            | never = Extract<keyof VehicleMapType, string>
    >(
        type: VehicleType
    ): VehicleMapType[VehicleType];
};

const mapper: Mapper<Vehicles> = null;

const vehicle1 = mapper.getVehicle('cars');
// I have a Car!
console.log(vehicle1);

const getVehicles = <
    VehicleMapType extends TypeMap = TypeMap,
    VehicleType extends Extract<keyof VehicleMapType, string> | never = Extract<
        keyof VehicleMapType,
        string
    >
>(
    type: VehicleType
): VehicleMapType[VehicleType] => {
    return null;
};

const vehicle2 = getVehicles<Vehicles>('cars');
// How can I get a Car instead of a Car | Plane?
console.log(vehicle2);

Thanks!

Edit

Besides, we also face an issue in the Mapper implementation as you can see in this TS Playground

Upvotes: 1

Views: 523

Answers (1)

jsejcksn
jsejcksn

Reputation: 33739

You either have to

  • supply both generics when invoking the function (in your example, you are only supplying Vechicles), or
  • use a functional abstraction to infer the types, like this:

TS Playground

type StringKeys = Record<string, any>;

type Named = { name: string };
type Plane = Named & { wings: number };
type Car = Named & { wheels: number };

type Vehicles = {
  cars: Car;
  planes: Plane;
};

type Mapper<T extends StringKeys> = {
  getVehicle<K extends keyof T>(type: K): T[K];
};

const mapper: Mapper<Vehicles> = {
  getVehicle (type) {
    const vechicles: Vehicles = {
      cars: {name: 'a car', wheels: 4},
      planes: {name: 'a plane', wings: 2},
    };
    const result = vechicles[type];
    if (result) return result;
    throw new Error('unknown vehicle type');
  }
};

const vehicle1 = mapper.getVehicle('cars'); // Car


// Generic functional abstraction:

const pickFrom = <T extends StringKeys>(o: T): <K extends keyof T>(key: K) => T[K] => {
  return key => o[key];
};

declare const vehicles: Vehicles;
const getVehicles = pickFrom(vehicles);
const vehicle2 = getVehicles('cars'); // Car

Upvotes: 2

Related Questions