首页 > 解决方案 > 嵌套索引类型导致“不能用于索引类型”错误

问题描述

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

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

操场

只是想检查一下 - 这是这个Stack Overflow 问题这个开放错误的欺骗吗?

(这些似乎与数组/元组类型有关,而我的是对象/映射)。

如果是这样,在我的情况下推荐的解决方案是什么?

如果不是,这里错误的原因是什么?

标签: typescript

解决方案


只是想检查一下 - 这是这个 Stack Overflow 问题和这个开放错误的欺骗吗?

您遇到的问题不是重复的。对元组和数组进行操作会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

打字稿必须支持一般情况并检查作为参数传递的所有可能值。最广泛的类型T extends ItemTypes"foo" | "bar"。碰巧在您的情况下keyof VersionMap<ItemTypes>"1",但在最通用的情况下,此类型可能为空(又名never),因此无法用于索引任何其他类型。

TS 将来有可能通过更好的推理引擎来支持您的用例。但它本身绝对不是一个错误 - 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

操场


推荐阅读