嵌套索引类型导致"不能用于索引类型"错误



我正在尝试创建一个对象,该对象是多个项键之间的关系,并且项键有不同的版本,它们有自己的数据类型。

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报告数组的所有键(例如lengthforEach(,而不仅仅是元组/数组的索引。你的问题有点微妙。


您的问题非常不直观,主要源于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参数,所以它不知道在运行时你是否会传递itemKeyversion对,而这对在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; 
}

这里有一个游乐场(如果我理解正确的话(应该尊重你的所有要求。

最新更新