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

时间:2021-01-12 05:08:30

标签: typescript

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

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

Playground

只是想检查一下 - 这是 Stack Overflow questionthis open bug 的骗局吗?

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

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

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

3 个答案:

答案 0 :(得分:2)

<块引用>

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

您遇到的问题不是重复的。对元组和数组进行操作 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 有可能通过更好的推理引擎来支持您的用例。但这绝对不是其本身的错误 - 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

PLAYGROUND

答案 1 :(得分:0)

我建议在 TypeScript 存储库上提交一个新问题。如果它是重复的,他们就会关闭它。至于推荐的解决方案,稍微调整泛型类型约束似乎可以解决问题:Playground

答案 2 :(得分:0)

即使我无法为您提供问题的实际答案,我也可以为您提供替代解决方案。

我认为(我说的是“我认为”,请不要将其视为事实)问题在于,在编译时 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; 
}

这是一个 Playground,它(如果我理解正确的话)应该尊重您的所有要求。