Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested index types causing a 'cannot be used to index type' error

Tags:

typescript

I'm trying to create an object that is a relationships between between several item keys, and item key has different versions that have their own data types.

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
            }
        }
    }
}

Here's my TypeScript:

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

Just want to check - is this a dupe of this Stack Overflow question and this open bug?

(These appear to relate to array/tuple types, whereas mine are objects/maps).

If so, what is the recommended solution in my case?

If not, what is the cause of the error here?

like image 711
dwjohnston Avatar asked Nov 19 '25 23:11

dwjohnston


1 Answers

Just want to check - is this a dupe of this Stack Overflow question and this open bug?

Issue you are experiencing is not a duplicate. Operating on tuples and arrays keyof reports all keys of an array (e.g. length, forEach) and not just indexes of the tuple / array. Your issue is a bit more nuanced.


Your issue is quite unintuitive and stems mostly from the way literal types are treated by Typescript. It feels that in your specific case TS should have enough information to infer that U can by used to index the underlying type. But please consider the general case:

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 must support the general case and check for all possible values passed as arguments. The broadest type for T extends ItemTypes is "foo" | "bar". It happens that in your case keyof VersionMap<ItemTypes> is "1", but in the most generic scenario this type might be empty (aka never) and as such not useable to index any other type.

It is possible TS would be able to support your use case with a better inference engine in the future. But it is definitely not a bug on its own - TS just takes a safer bet here.


Below, I present a possible solution while trying to keep close to the original intention. The solution basically entangles parameters as a [type, version] pair and proves with a conditional type the pair might be used to index the nested structure. It feels to me that it could be simplified a bit further. Actually my preferred way would be to start with value representing the most nested structure and create types from it (starting with typeof) and try not to use / introduce redundant information - but it is a bit out of scope for the original question.

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

like image 187
artur grzesiak Avatar answered Nov 23 '25 10:11

artur grzesiak