'Typed Indexing variable is not of the same type as the object being indexed
I have a slightly complex object that has typed keys on the top-most level of the object, as well as the deepest level of the object, and I can't seem to be able to index the second typed keys of the object.
This is my object and my interfaces (simplified):
const obj = {
keyA: {
variants: {
"0": false,
"1": false
}
},
keyB: {
variants: {
"0-0": false,
"0-1": false
}
}
}
type Obj = typeof obj;
type ObjKeys = keyof Obj;
type ObjVariants<T extends ObjKeys> = Obj[T]["variants"];
and this is the function that cannot properly index selectedKeys.variant
using defaultVariant
.
function getVariant<T extends ObjKeys>(key: T, defaultVariant: keyof ObjVariants<T>) {
const selectedKey = obj[key];
const selectedVariant = selectedKey.variants[defaultVariant];
}
The error I get is:
Type 'keyof { keyA: { variants: { 0: boolean; 1: boolean; }; }; keyB: { variants: { "0-0": boolean; "0-1": boolean; }; }; }[T]["variants"]' cannot be used to index type '{ 0: boolean; 1: boolean; } | { "0-0": boolean; "0-1": boolean; }'.(2536)
It seems to be related to these two issues: https://github.com/microsoft/TypeScript/issues/21760 and https://github.com/microsoft/TypeScript/issues/36631 But I'm not sure that the "workaround" for the second one works for me, I feel like I'm missing something
Has anyone else encountered something similar/has a suggested workaround?
PD: I tried adding a strict type to obj
and found the same result as using typeof obj
.
Solution 1:[1]
Looks like behavior by design, or in other words design limitation. Such function is hard to be type safe as we can pass subtypes also. For example T extends ObjKeys
covers also the union keyA | keyB
, it means that this constraint cannot assume it will be one of variants.
In order to totally set the constraint we can go into following approach:
function getVariant
<O extends { [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } }
, K1 extends keyof Obj
, K2 extends keyof Obj[K1]["variants"]>
(o: O, key: K1, defaultVariant: K2) {
const selectedKey = o[key].variants;
const selectedVariant = selectedKey[defaultVariant];
}
// using
getVariant(obj, 'keyA', '0')
As you can see I made additional parameter in order to narrow the type for keys K1
and K2
. The most important line is { [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } }
. It means that we deal with type which covers both keys, but still the definition is compatible with our obj
type.
We can still be using obj
as a outside constant, but then we need to use type assertion:
function getVariant<K1 extends keyof Obj, K2 extends keyof Obj[K1]["variants"]>
(key: K1, defaultVariant: K2) {
let _obj = obj as { [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } };
const selectedKey = _obj[key].variants;
const selectedVariant = selectedKey[defaultVariant];
}
// using
getVariant('keyA', '0')
Why this works? It works because we statically set the type with exact types we have/will have. If we say T extends A
it doesn't mean T is A, it doesn't mean also that T is some variant of A. By saying T extends A
we narrow, restrict the type T
to be assignable to A
, therefor we are not strict. By making type {[K in T]: X}, T extends Y
we strictly define that the type will have exact key of T
, and it will be strictly one type from the possible assignable types to Y
. Such strict definition is equal to setting static value like Obj['keyA']
.
Solution 2:[2]
I am a bit late to the party, but the following generalization could solve the problem:
type KeyOf<T> = keyof T;
function getVariant<
O extends { [K in KeyOf<O>]: { variants: O[K]["variants"] } },
K extends KeyOf<O>,
V extends KeyOf<O[K]["variants"]>,
>(obj: O, key: K, variant: V): O[K]["variants"][V] {
return obj[key].variants[variant];
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Andrea Simone Costa |