打字稿:如何根据对象键/值类型在 ES6 映射中创建条目

Posted

技术标签:

【中文标题】打字稿:如何根据对象键/值类型在 ES6 映射中创建条目【英文标题】:Typescript: How can I make entries in an ES6 Map based on an object key/value type 【发布时间】:2019-07-21 06:22:34 【问题描述】:

我想使用 Map 而不是 object 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]
]);

【问题讨论】:

Map 并没有真正以这种方式输入以使这项工作适合您。您需要提出自己的自定义类型interface MyMap ...interface MyMapConstructor ...,它们可能是也可能不是MapMapConstructor 的实际子类型。对于一些工作量太大了,当你完成所有工作后,它会为你提供与 Values 类型相同的功能。你需要多少这个? 编辑:在原始帖子中添加了更多代码。我更喜欢上面的结构而不是对象,因为我正在考虑使用符号作为键,并且因为它看起来更加结构化/严格。 第一个例子对我来说似乎不是错误 正确的胡安,我的第一个用例实际上已经解决了。我还考虑过将这些键、值声明为具有严格类型检查的数组,然后对 Maps 进行不那么严格的类型检查,但我不确定它是否会起作用。 符号可以是对象键:typescriptlang.org/docs/handbook/symbols.html 【参考方案1】:

这是我能想象得到的最接近的,虽然我仍然不明白为什么我们不只使用普通对象开始:

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 OO[K] 来表示KVMap&lt;K, V&gt; 中通常表示的类型。

构造函数有点烦人,因为类型推断不能按照您想要的方式工作,因此保证类型安全分为两个步骤:

// 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&lt;Values&gt; 不匹配(例如,您缺少键或值类型错误),那么 myMap 会给您错误。

现在您可以将myMap 用作ObjectMap&lt;Values&gt;,类似于使用Map 实例的方式,以及get()set(),它应该是类型安全的。

请再次注意...对于一个更复杂的对象来说,这似乎需要做很多工作,而且类型更复杂,而且没有比普通对象更多的功能。我会严重警告任何使用Map 其键是keyof any 子类型(即string | number | symbol)的人强烈警告consider using a plain object,并确保您的用例确实需要Map

Playground link to code

【讨论】:

感谢您抽出宝贵的时间来解释这是非常详细的。它现在可以正常工作,但我将不得不考虑是否值得在普通对象上使用,特别是因为我可能必须将 key 转换为 key。 那么我认为这将是一个可行的选择,尤其是因为 Map 可以确保插入顺序。 您关心插入顺序吗? ObjectMap&lt;O&gt; 不知道 O 中键的顺序,因此在编译时它不会帮助您。不知道你为什么关心订单,但我可以想象一种类型可以保证使用元组类型而不是数组的订单......但这更加复杂,我想我现在需要尖叫着逃跑。 ?‍♂️? 哈哈哈,好吧,现在想起来,我可能吓到你了?插入顺序并不重要,因为它是一张地图,所以每个键只能表示一次。

以上是关于打字稿:如何根据对象键/值类型在 ES6 映射中创建条目的主要内容,如果未能解决你的问题,请参考以下文章

如何使用打字稿中的查找来推断类型化的 mapValues?

打字稿如何定义对象,其中键的值取决于另一个键

来自键值对象的打字稿类型函数

打字稿:如何将对象映射到类型?

如何使用带有动态对象键的打字稿

打字稿不正确的分配/映射到强类型对象[关闭]