3

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?

1
  • Same problem here. Commented Jan 18, 2024 at 1:59

3 Answers 3

4
+200

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

Sign up to request clarification or add additional context in comments.

1 Comment

Sorry it looks like yoru playground link is wrong. I posted your code in as it currently is, and there's an type error on ` return myMap[args[0]][args[1]]`
0

Even though I can't provide you the actual answer to your question I can propose you an alternative solution.

I think (I said "I think", please don't take this as the truth) that the problem is that at compile time TypeScript can't know what you'll pass as itemKey argument, so it can't know if at run time you'll pass a itemKey, version couple which doesn't exist in myMap object...

Once said that, my solution proposal:

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

Here is a Playground which (if I understtod correctly) should respect all your requirements.

Comments

-1

I'd recommend filing a new issue on the TypeScript repository. If it is a duplicate, they'll just close it. As for a recommended solution, adjusting the generic type constraints a bit seems to do the trick: Playground

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.