Typescript:如何基于对象的键/值类型向 ES6 Map 中添加条目

9
我希望使用Map来代替对象映射来声明一些键和值。但是Typescript似乎不支持ES6 Map的索引类型,这是正确的吗?有没有解决办法?
此外,我还希望使值具有类型安全性,以便映射中的每个条目都具有与键对应的正确类型。
下面是一些伪代码,描述了我所试图实现的内容:
type Keys = 'key1' | 'key2';

type  Values = {
  'key1': string;
  'key2': number;
}

/** Should display missing entry error */
const myMap = new Map<K in Keys, Values[K]>([
  ['key1', 'error missing key'],
]);

/** Should display wrong value type error for 'key2' */
const myMap = new Map<K in Keys, Values[K]>([
  ['key1', 'okay'],
  ['key2', 'error: this value should be number'],
]);

/** Should pass */
const myMap = new Map<K in Keys, Values[K]>([
  ['key1', 'all good'],
  ['key2', 42],
]);

编辑:更多的代码部分描述了我的使用情况

enum Types = {
  ADD = 'ADD',
  REMOVE = 'REMOVE',
};

/** I would like type-safety and autocompletion for the payload parameter */
const handleAdd = (state, payload) => ({...state, payload});

/** I would like to ensure that all types declared in Types are implemented */
export const reducers = new Map([
  [Types.ADD, handleAdd],
  [Types.REMOVE, handleRemove]
]);

4
“Map”并没有以这种方式进行类型设置,使它适用于您的情况。您需要自己创建自定义类型“interface MyMap {...}”和“interface MyMapConstructor {...}”,这些类型可能是“Map”和“MapConstructor”的实际子类型,也可能不是。为了得到与您的“Values”类型相同的功能,这需要大量的工作。您有多需要这个? - jcalz
我更喜欢上述的结构而不是对象,因为我正在考虑使用Symbols作为键,并且因为它看起来更加结构化/严格。 - Nikolai Hegelstad
正确,Juan,我的第一个用例已经解决了。我还考虑将这些键值声明为具有严格类型检查的数组,然后在映射上进行较少严格的类型检查,但我还不确定它是否有效。 - Nikolai Hegelstad
据我所知,对象映射是从字符串 -> 对象的。对于reducers,我希望函数的参数可以推断出它们将接收哪种类型作为有效负载。我的actions符合FSA标准,具有类型和有效负载作为属性。 - Nikolai Hegelstad
1
符号可以作为对象键:https://www.typescriptlang.org/docs/handbook/symbols.html - jcalz
显示剩余2条评论
1个回答

8
这是我能想到的最接近的方式,虽然我仍然不理解为什么我们不直接使用简单对象开始:
type ObjectToEntries<O extends object> = { [K in keyof O]: [K, O[K]] }[keyof O]

interface ObjectMap<O extends object> {
  forEach(callbackfn: <K extends keyof O>(
    value: O[K], key: K, map: ObjectMap<O>
  ) => void, thisArg?: any): void;
  get<K extends keyof O>(key: K): O[K];
  set<K extends keyof O>(key: K, value: O[K]): this;
  readonly size: number;
  [Symbol.iterator](): IterableIterator<ObjectToEntries<O>>;
  entries(): IterableIterator<ObjectToEntries<O>>;
  keys(): IterableIterator<keyof O>;
  values(): IterableIterator<O[keyof O]>;
  readonly [Symbol.toStringTag]: string;
}

interface ObjectMapConstructor {
  new <E extends Array<[K, any]>, K extends keyof any>(
    entries: E
  ): ObjectMap<{ [P in E[0][0]]: Extract<E[number], [P, any]>[1] }>;
  new <T>(): ObjectMap<Partial<T>>;
  readonly prototype: ObjectMap<any>;
}

const ObjectMap = Map as ObjectMapConstructor;

新接口 ObjectMap 的想法是,它特别依赖对象类型 O 来确定其键/值关系。然后,你可以说 Map 构造函数可以充当 ObjectMap 构造函数。我还删除了任何可以更改实际存在的键的方法(并且 has() 方法也是多余的 true)。
我可以费心解释每个方法和属性定义,但这需要大量的类型处理。简而言之,您要使用 K extends keyof O 和 O[K] 来表示通常由 Map 中的 K 和 V 表示的类型。
构造函数有点麻烦,因为类型推断不按您希望的方式工作,因此保证类型安全分为两个步骤:
// let the compiler infer the type returned by the constructor
const myMapInferredType = new ObjectMap([
  ['key1', 'v'], 
  ['key2', 1],  
]);

// make sure it's assignable to `ObjectMap<Values>`: 
const myMap: ObjectMap<Values> = myMapInferredType;

如果您的myMapInferredTypeObjectMap<Values>不匹配(例如,您缺少键或值类型错误),那么myMap将会给您带来错误。
现在,您可以像使用Map实例一样,使用get()set()访问myMap作为ObjectMap<Values>,并且它应该是类型安全的。
请再次注意... 对于一个更复杂、具有更棘手类型的对象来说,这似乎是很多工作,而且没有比普通对象更多的功能。我会严重警告任何使用其键是keyof any子类型(即string | number | symbol)的Map的人们,要考虑使用普通对象,并确保您的使用情况真正需要Map

代码的Playground链接


感谢您花费时间详细解释这个问题。现在它可以正常工作,但我必须考虑是否值得使用它来代替普通对象,特别是因为我可能需要将键强制转换为key。 - Nikolai Hegelstad
那么我认为这将是一个可行的选项,特别是因为Map确保插入顺序。 - Nikolai Hegelstad
你关心插入顺序吗?ObjectMap<O> 不知道 O 中键的顺序,因此它在编译时无法帮助你解决这个问题。不确定为什么你关心顺序,但我可以想象一种类型,使用元组类型而不是数组来保证顺序...但这更加复杂,我想现在我需要尖叫着逃跑了。 ‍♂️ - jcalz
2
@jcalz 自那时起,TypeScript 有什么变化吗?我也非常希望能够使用 Map,因为我需要能够对属性进行排序。 - Łukasz Zaroda
@ŁukaszZaroda 我所知道的情况并没有改变。我不确定你在这里所指的“排序”是什么意思。根据您的用例,可能可以为其编写自定义类型,但评论部分不是探索该问题的好地方。您可能需要为此发布自己的问题帖子。 - jcalz
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接