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);
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