我正在尝试创建一个对象,该对象是多个项键之间的关系,并且项键有不同的版本,它们有自己的数据类型。
const myMap : MyMapObject = {
"foo": {
"1": {
type: "foo",
version: "1",
data: {
name: "bob"
}
},
"2" : {
type: "foo",
version: "2",
data: {
firstName: "Bob",
lastName: "Jones"
}
}
},
"bar" : {
"1": {
type: "bar",
version: "1",
data: {
age: 1
}
}
}
}
这是我的打字稿:
type ItemTypes = "foo" | "bar";
type Version = string;
type Foo = {
"1": {
name: string;
};
"2": {
firstName: string;
lastName: string;
}
}
type Bar = {
"1": {
age: number;
}
}
type BaseTypeMap = {
"foo": Foo;
"bar": Bar;
}
type VersionMap<T extends ItemTypes> = BaseTypeMap[T];
type ItemObject<T extends ItemTypes, U extends keyof VersionMap<T>> = {
type: T;
version: U;
data: VersionMap<T>[U];
}
type MyMapObject = {
[K in ItemTypes] : {
[J in keyof VersionMap<K>] : ItemObject<K, J>;
}
}
function getDataForTypeAndVersion<T extends ItemTypes, U extends keyof VersionMap<T>> (itemKey: T, version: U) : ItemObject<T,U> {
const versionMap = myMap[itemKey] ;
const item = versionMap[version]; //Type 'U' cannot be used to index type 'MyMapObject[T]'.(2536) <-- Error here
return item;
}
//The function appears to work fine.
const foo1 = getDataForTypeAndVersion("foo", "1");
foo1.data.name;
foo1.data.firstName; //expected error
const foo2 = getDataForTypeAndVersion("foo", "2");
const foo3 = getDataForTypeAndVersion("foo", "3"); //expected error
const bar1 = getDataForTypeAndVersion("bar", "1");
const char1 = getDataForTypeAndVersion("chaz", "1"); //expected error
游乐场
只想检查一下——这是这个堆栈溢出问题和这个打开的bug的翻版吗?
(这些似乎与数组/元组类型有关,而我的是对象/映射(。
如果是,在我的情况下,建议的解决方案是什么?
如果没有,这里的错误原因是什么?
只想检查一下——这是这个堆栈溢出问题和这个打开的bug的翻版吗?
您遇到的问题并非重复。对元组和数组进行操作keyof
报告数组的所有键(例如length
、forEach
(,而不仅仅是元组/数组的索引。你的问题有点微妙。
您的问题非常不直观,主要源于Typescript处理文字类型的方式。它认为,在您的特定情况下,TS应该有足够的信息来推断U可以用于索引底层类型。但请考虑一般情况:
const fooOrBar = "foo" as ItemTypes;
const barOrFoo = getDataForTypeAndVersion(fooOrBar, "2"); // an error since TS does not know what is exact type of the first argument
Typescript必须支持一般大小写,并检查作为参数传递的所有可能值。T extends ItemTypes
最广泛的类型是"foo" | "bar"
。在您的案例中,keyof VersionMap<ItemTypes>
恰好是"1"
,但在最通用的场景中,该类型可能是空的(也称为never
(,因此不可用于索引任何其他类型。
TS将来可能能够用更好的推理引擎来支持您的用例。但它本身绝对不是一个bug——TS只是在这里下了一个更安全的赌注。
下面,我提出了一个可能的解决方案,同时努力保持接近初衷。该解决方案基本上将参数纠缠为[type, version]
对,并用条件类型证明该对可以用于索引嵌套结构。我觉得它可以进一步简化。实际上,我更喜欢的方法是从表示嵌套最多的结构的值开始,并从中创建类型(从typeof
开始(,尽量不使用/引入冗余信息,但这有点超出了最初问题的范围。
type ItemTypes = "foo" | "bar";
type Foo = {
"1": {
name: string;
};
"2": {
firstName: string;
lastName: string;
}
}
type Bar = {
"1": {
age: number;
}
}
type BaseTypeMap = {
"foo": Foo;
"bar": Bar;
}
type VersionMap<T extends ItemTypes> = BaseTypeMap[T];
type ItemObject<A extends MyMapObjectParams> = {
type: A[0];
version: A[1];
// data: BaseTypeMap[A[0]][A[1]]; // the same TS2536 error
data: A[1] extends keyof BaseTypeMap[A[0]] ? BaseTypeMap[A[0]][A[1]] : never; // a way to make TS happy by proving we are able to index the underlying type
}
type MyMapObject = {
[K in ItemTypes] : {
[J in keyof VersionMap<K>] : [K, J] extends MyMapObjectParams ? ItemObject<[K, J]> : never;
}
}
type MyMapObjectParams = {
[K in ItemTypes] : {
[J in keyof VersionMap<K>] : [type: K, version: J]
}[keyof VersionMap<K>]
}[ItemTypes]
const myMap : MyMapObject = {
"foo": {
"1": {
type: "foo",
version: "1",
data: {
name: "bob"
}
},
"2" : {
type: "foo",
version: "2",
data: {
firstName: "Bob",
lastName: "Jones"
}
}
},
"bar" : {
"1": {
type: "bar",
version: "1",
data: {
age: 1
}
}
}
}
function getDataForTypeAndVersion <A extends MyMapObjectParams>(...args: A) : ItemObject<A> {
return myMap[args[0]][args[1]]
}
const foo1 = getDataForTypeAndVersion("foo", "1");
foo1.data.name;
foo1.data.firstName; //expected error
const foo2 = getDataForTypeAndVersion("foo", "2");
const foo3 = getDataForTypeAndVersion("foo", "3"); //expected error
const bar1 = getDataForTypeAndVersion("bar", "1");
const bar2 = getDataForTypeAndVersion("bar", "2"); // expected error
const char1 = getDataForTypeAndVersion("chaz", "1"); //expected error
游乐场
我建议在TypeScript存储库中提交一个新问题。如果它是重复的,他们会关闭它。至于推荐的解决方案,稍微调整一下通用类型约束似乎可以奏效:Playground
即使我不能为您提供问题的实际答案,我也可以为您提出一个替代解决方案。
我认为(我说了"我认为",请不要把这当成事实(问题是在编译时TypeScript不知道你将传递什么作为itemKey
参数,所以它不知道在运行时你是否会传递itemKey
、version
对,而这对在myMap
对象中不存在。。。
曾经说过,我的解决方案建议:
function getDataForTypeAndVersion<M extends MyMapObject, T extends keyof M, U extends keyof M[T]> (map: M, itemKey: T, version: U) : ItemObject<T,U> {
const versionMap = map[itemKey];
const item = versionMap[version];
return item;
}
这里有一个游乐场(如果我理解正确的话(应该尊重你的所有要求。