Home > other >  Typescript: Generic over Map<K,V> value type
Typescript: Generic over Map<K,V> value type

Time:10-15

Here's a simplified version of the problem. I'm trying to create a function that will operate generically on an interface with Map properties.

interface Data {
  map1: Map<string, number>;
  map2: Map<string, string>;
}

const d: Data = {
  map1: new Map(),
  map2: new Map(),
};

// This doesn't work
public dataSet<T extends keyof Data, V extends Data[T]... (need help here)>(d: data, dataKey: T, key: string, val: V) {
  d[dataKey].set(key, val);
}

// Ideally, called like:
dataSet(d, 'map1', 'alpha', 3);
dataSet(d, 'map2', 'beta', 'charlie');

The type definition of V is currently the Map type rather than the Map's value type.

CodePudding user response:

How about using objects instead of Maps ?

interface Data {
  map1: {
    [K: string]: number
  };
  map2: {
    [K: string]: string
  };
}

const d: Data = {
  map1: {},
  map2: {},
};

function dataSet<T extends keyof Data, V extends Data[T][string]>(d: Data, dataKey: T, key: string, val: V) {
  d[dataKey][key] = val;
}


// works:
dataSet(d, 'map1', 'alpha', 3);
dataSet(d, 'map2', 'beta', 'charlie');

// Errors
dataSet(d, 'map1', 'alpha', 'charlice');
dataSet(d, 'map2', 'beta', 3);

playground

CodePudding user response:

You can use some conditional types to infer the key and value types of your Maps. You'll run into a type issue when calling set because TS is inferring d[dataKey] as a Map<string, number> | Map<string, string>. But we know what we're doing here so we can safely add a @ts-expect-error line.

I tested this on TS v4.1.5 on the playground.

interface Data {
  map1: Map<string, number>;
  map2: Map<string, string>;
}

const d: Data = {
  map1: new Map(),
  map2: new Map(),
};

type GetKey<M extends Map<any, any>> = M extends Map<infer K, any> ? K : never;
type GetValue<M extends Map<any, any>> = M extends Map<any, infer V> ? V : never;

function dataSet<K extends keyof Data>(d: Data, dataKey: K, key: GetKey<Data[K]>, val: GetValue<Data[K]>) {
  // @ts-expect-error
  d[dataKey].set(key, val);
}

// This now enforces the types correctly.
dataSet(d, 'map1', 'alpha', 3);
dataSet(d, 'map2', 'beta', 'charlie');
dataSet(d, 'map2', 'foo', true); // Errors as expected.

Here's a link to the playground as well.

CodePudding user response:

It is possible to do with help of inference. You should infer whole data structure. Starting from Map key.

interface Dictionary {
  map1: Map<string, number>;
  map2: Map<string, string>;
}

const d: Dictionary = {
  map1: new Map(),
  map2: new Map(),
};

type InferMap<T extends Map<any, any>> = T extends Map<infer Key, infer Value> ? [Key, Value] : never

// This doesn't work
const dataSet = <
  Key extends string,
  Value extends string | number,
  HashMapKey extends string,
  HashMap extends Record<HashMapKey, Map<Key, Value>>,
  >(hashMap: HashMap, dataKey: HashMapKey) =>
  (key: InferMap<HashMap[HashMapKey]>[0], val: InferMap<HashMap[HashMapKey]>[1]) =>
    hashMap[dataKey].set(key, val);

// Ideally, called like:
dataSet(d, 'map1')('alpha', 2) // ok
dataSet(d, 'map1')('alpha', 'str') // expected error

dataSet(d, 'map2')('beta', 'charlie');
dataSet(d, 'map2')('beta', 4); // expected error

HashMapKey - infers map1 and map2

HashMap - infers whole data structure (argument)

InferMap - infers map key and map value and returns tuple, where first element is a key and second, accordingly is a value. Playground

If you are interested in function argument inference, you can check my article

  • Related