如何在地图值中反向引用地图键?

  • 本文关键字:地图 引用 typescript
  • 更新时间 :
  • 英文 :


我有以下类型:

type mapOptions = {
'a': {},
'b': {
'somethingElse': string,
'somethingDiff': number
},
'c': {
'somethingC': string
}
}

现在我想创建一个 Map,它可以将其键设置为我的对象的键,并占用特定对象的值,如下所示:

type innerMapObject<T extends keyof mapOptions> {
status: boolean,
options: mapOptions[T]
}

基本上,我想要的是,在我拥有地图之后,在获取或设置其中的值时,获取正确的底层选项类型:

const MyMap: Map<...> = new Map();
MyMap.set('d', {}); // error here, "d" is not a property of "mapOptions";
MyMap.set('a', {}); // works correctly
MyMap.set('c', {
"somethingD": "test"
}); // error here, the value object does not match "mapOptions["c"]"
/** This should be of type:
*
*  {
*      status: boolean,
*      options: {
*         somethingElse: string,
*         somethingDiff: number
*      }
*  }
*
*
*/
const myBValue = MyMap.get("b");

是否可以以某种方式在与该键关联的值中反向引用映射的键?

是的,尽管代码可能看起来有点奇怪。 基本上,Map类型采用两个泛型:K用于键类型,V用于值类型。 这类似于Record<K, V>. 但是,由于布局的原因,无法说来自对象的某些 props 具有某些值,因为所有键都具有相同的值类型。

但是,有一个解决方法,因为您可以创建Map的交集,以使 TypeScript 推断重载签名以允许特定键具有特定值。

type MyMap = Map<"planet", { name: string; size: number }> & Map<"person", { name: string; age: number }>;
const myMap: MyMap = new Map();
// these work   
myMap.set("planet", {
name: "Mars",
size: 21,
});
myMap.set("person", {
name: "Jon Doe",
age: 21,
});
// these fail   
myMap.set("invalid_key", 2);
myMap.set("person", {
name: "Hey",
size: 2, // notice I'm setting `size` but it should have `age`
});

打字稿游乐场链接

虽然,如果您有多个属性,则手动编写此代码可能相当少。 因此,我创建了一个帮助程序类型,当它从对象类型生成Map<K, V>时,它应该更容易。 它接受[key, value]元组数组类型中的键和值。 我使用数组类型的原因是,如果您使用常规对象,那么键类型将限制为PropertyKey(string | symbol | number),但Map允许各种值作为键。

// https://stackoverflow.com/a/50375286/10873797
type UnionToIntersection<U> = 
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type CreateMap<T extends readonly (readonly [unknown, unknown])[]> = 
UnionToIntersection<
T[number] extends infer $Value // make distributive
? $Value extends readonly unknown[]
? Map<$Value[0], $Value[1]>
: never
: never
>;
type MyMap = CreateMap<[
["planet", { name: string; size: number }],
["person", { name: string; age: number }],
]>;
const myMap: MyMap = new Map();
// these work   
myMap.set("planet", {
name: "Mars",
size: 21,
});
myMap.set("person", {
name: "Jon Doe",
age: 21,
});
// these fail   
myMap.set("invalid_key", 2);
myMap.set("person", {
name: "Hey",
size: 2, // notice I'm setting `size` but it should have `age`
});

打字稿游乐场链接

编辑:解释

我的解决方案使用分发类型。 在CreateMap类型中,它接受键和值的元组,如下所示:

type CreateMap<Init extends readonly (readonly [unknown, unknown])[]>
= Init[number];
// ["hey", "you"] | ["me", "too"]
type Foo = CreateMap<[
["hey", "you"],
["me", "too"]
]>;

但是,如果您尝试简单地将这些值插入Map<K, V>中,那么您将失去有关哪个键等于什么值的细节的上下文:

type CreateMap<Init extends readonly (readonly [unknown, unknown])[]>
= Map<Init[number][0], Init[number][1]>;
//  Map<"hey" | "me", "you" | "too">
type Foo = CreateMap<[
["hey", "you"],
["me", "too"]
]>;

因此,我们需要一种方法来枚举元组中的项作为顶级联合。 然后,为每个项目和密钥对显式创建一个Map。 此图可能是解释它的好方法:

// current way
[["hey", "you"], ["me", "too"]] => Map<"hey" | "you", "me" | "too">
// what we want
[["hey", "you"], ["me", "too"]] => Map<"hey", "me"> | Map<"me", "too">

这可以通过将我们的条件类型分布在Init[number]上使用,这将为Init元组类型中的每个项目运行条件。 为此,我将通过使用infer _简单地为值创建一个类型别名来做到这一点。 但是,当您推断时,您会丢失类型的上下文,因此我们必须添加另一个检查来断言推断类型是元组以获取键和值类型:

type CreateMap<Init extends readonly (readonly [unknown, unknown])[]> = 
Init[number] extends infer $Entry
? $Entry extends readonly [infer $K, infer $V]
? Map<$K, $V>
: never
: never;
// Map<"hey", "you"> | Map<"me", "too">
type Foo = CreateMap<[
["hey", "you"],
["me", "too"]
]>;

但是,正如您所注意到的,我们的类型是地图的联合。我们需要它是一个交集,以便我们知道映射将同时指定所有键/值模式。 我们可以通过使用@jcalz的UnionToIntersection类型来做到这一点。

// https://stackoverflow.com/a/50375286/10873797
type UnionToIntersection<U> = 
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type CreateMap<Init extends readonly (readonly [unknown, unknown])[]> = 
UnionToIntersection<
Init[number] extends infer $Entry
? $Entry extends readonly [infer $K, infer $V]
? Map<$K, $V>
: never
: never
>;
// Map<"hey", "you"> & Map<"me", "too">
type Foo = CreateMap<[
["hey", "you"],
["me", "too"]
]>;

打字稿游乐场链接

最新更新