【问题标题】:How to define interface according to the keys provided by Map?如何根据 Map 提供的键定义接口?
【发布时间】:2019-07-14 06:48:55
【问题描述】:

我是 Typescript 的新手。

我想提取一个名为 Mapper 的类。
这个映射器有两个参数:
1.我们要映射的对象数据 2. 定义map后数据形状的Map

它有一些方法可以根据我们想要做的映射返回一个 MappedData。

class Mapper {
  constructor(data: Data, dataMap: Map) {

  }

  mapKey(): MappedKeyData {
    // return a mappedKeyData
  }
}

// Usage
const source = {
  Name: "Dudi"
}
const map = new Map([
  ['name', 'Name']
])
const mapper = new Mapper(source, map)
console.log(mapper.mapKey()) // {name: 'Dudi'}

问题是我想确保 MappedKeyData 的类型是一个接口,它具有在键映射之后定义的所有键。 (根据地图中定义的键)

例如:

interface MappedKeyData {
  name: 'string'
}

这是使用 Typescript 的正确方法吗?如果是,如何实施?谢谢...

【问题讨论】:

  • source 的属性没有在map 中命名时会发生什么情况,反之亦然?比如new Mapper({a: 1}, new Map(["b": "c"]))?
  • 同样奇怪的是map 有条目,其中 key 是所需的 output 键,而 value 是所需的 input 键。我想这将是相反的。但这一切都取决于您希望在地图与源不对应的边缘情况下看到什么。
  • 最后,如果键只是字符串,为什么还要Map?像{name: "Name"} 这样的普通旧对象不是更容易更直接吗? TypeScript 对跟踪普通对象类型(例如,{x: number, y: boolean})中的键值关系有很好的支持,但它把 Map 视为一个字典类型(例如,{[k: string]: number | boolean})。跨度>
  • 嗯...假设source 必须在地图中命名。我想要实现的目标是在 Typescript 中编写一个密钥替换器。所以如果我有一个对象{name: 'Dudi'},我可以将键索引替换为{'NAME": 'Dudi'},但是我不知道如何正确编写它。只要 key replace 可以工作,任何类型的 Map 都可以。你知道怎么做吗? @jcalz

标签: typescript


【解决方案1】:

如果你真的想使用类和 Map 对象,你可以,但使用函数和普通的旧 javascript 对象会更直接。
我将展示一种方法,您可以根据需要对其进行调整(但请注意,TypeScript 中的Map 没有按照您想要的方式进行强类型化,因此我会避免使用它)。

这是mapKeys()函数:

function mapKeys<D extends object, M extends Record<keyof M, keyof D>>(
  data: D,
  dataMap: M
) {
  const ret = {} as { [K in keyof M]: D[M[K]] };
  (Object.keys(dataMap) as Array<keyof M>).forEach(
    <K extends keyof M>(k: K) => {
      ret[k] = data[dataMap[k]];
    }
  );
  return ret;
}

这是它的基本用法:

const mapped = mapKeys(
  {
    Name: "Dudi",
    Age: 123
  },
  { name: "Name", age: "Age" }
);
// const mapped: {
//    name: string;
//    age: number;
// }
console.log(mapped); // {name: 'Dudi', age: 123}

mapKeys() 在运行时的工作方式是简单地循环遍历 dataMap 的键 k 并将返回对象中的类似属性设置为来自 datadataMap[k] 的数据。这就是ret[k] = data[dataMap[k]] 行。

剩下的大部分是类型注解和断言来帮助编译器理解和验证我们在做什么。假设data 的类型为DdataMap 的类型为M,则返回类型将为mapped type{[K in keyof M]: D[M[K]] }。这就是说它将具有与M 相同的键,并且每个键K 的值将是D[M[K]] 类型(即我们look up KK 属性M,并且然后查找D那个属性)。

所以这应该适合你,具体取决于你的用例。


当然有很多注意事项;我试图询问您在其中一些情况下想看到什么,我所知道的是,如果它不是 data 的键,您不希望在 dataMap 中放置一个值。 constraint M extends Record&lt;keyof M, keyof D&gt; 应该保证这一点。但还有其他可能的极端情况:

强输入dataMap

首先,dataMap 参数必须是相当强的类型才能工作。如果你这样做:

const source = { Name: "Didu", Age: 321 };
const map = { name: "Name", age: "Age" }; 

然后map 最终将被推断为类型{name: string, age: string} 而不是{name: "Name", age: "Age"}。你会得到一个错误:

const badInfer = mapKeys(source, map); // error!
// ┌───────────────────────────> ~~~
// └─ Argument of type '{ name: string; age: string; }' is not assignable
//    to parameter of type 'Record<"name" | "age", "Name" | "Age">'.

解决此问题的方法是确保dataMap 具有string literal 属性值,例如您在使用const assertion 时获得的值:

const fixedMap = { name: "Name", age: "Age" } as const; //
// inferred to be {readonly name: "Name"; readonly age: "Age"; }
const fixedInfer = mapKeys(source, fixedMap); // okay, right type now

请注意,如果我们使用Map 而不是对象,default TypeScript type declarations for it 不够强大。 Map&lt;K, V&gt; 意味着K 中的任何键都可能映射到V 中的任何值。所以new Map([["name","Name"],["age","Age"]]) 充其量最终会等同于{name: "Name" | "Age", age: "Name" | "Age"} 类型,而你的输出类型中会出现一个不幸的string | number

省略属性

没有什么能阻止您将data 中的一些键留在dataMap 之外:

const fewerProps = mapKeys({ Name: "Dodi", Age: 132 }, { name: "Name" });
// const fewerProps: { name: string }; // no age
console.log(fewerProps); // {name: "Dodi"}

重复的属性

没有什么能阻止您在data 中多次映射dataMap 中的同一个键:

const duplicateProps = mapKeys(
  { Name: "Dido", Age: 231 },
  {
    name: "Name",
    alsoName: "Name",
    age: "Age",
    alsoAge: "Age"
  }
);
// const duplicateProps: {name: string; alsoName: string; age: number; alsoAge: number;}
console.log(duplicateProps); // {name: "Dido", alsoName: "Dido", age: 231, alsoAge: 231}

索引签名会做奇怪的事情

如果你的data 对象有一个index signature,那么类型系统的行为会很奇怪,因为它会假设data 有任何可能与索引匹配的键:

const dictionary: { [k: string]: string | number } = { Name: "Didi", Age: 312 };
const confused = mapKeys(dictionary, {
  dogs: "Dogs",
  cats: "Cats",
  age: "Age"
});
// const confused: { dogs: string | number; cats: string | number; age: string | number;}
console.log(confused); // {dogs: undefined, cats: undefined, age: 312 }
// note that "undefined" can sneak into index signatures

这不是一个完美的结果,因为类型系统说confused 有一个dogs 类型为string | number 的属性,但在运行时它实际上是undefined。这是索引签名的陷阱之一,我们的映射器就陷入其中。


如果您决定要查看什么并放入更复杂的类型断言和注释,则可以解决其中的一些警告,但我想我会在这里停下来,并将任何修补工作留给您。这应该足以给你一些方向。祝你好运!

Link to code

【讨论】:

  • 这是我见过的最好的答案!谢谢,请允许我称您为 @jcalz 大师。
猜你喜欢
  • 1970-01-01
  • 2021-11-24
  • 1970-01-01
  • 1970-01-01
  • 2016-03-18
  • 2018-12-30
  • 2019-05-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多