'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

Here's the link to the playground: https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=65&pc=3#code/MYewdgzgLgBCBGArGBeGBvAUDGBrApgJ4CCAXBtjjAG4CGATgJa1hQTlZVUBEADN+QBmtADYR8AGkpduARgExhY-NJgBfSmqk4ChAEIdVdJizaGuOPgFp+Q0eO0Xr8u8tUacGjZiiEADvgwAPJIqDC+ASCCcEgA3D7+gSGIANJEEGG6UcFxCQE5iABqDMysEAA8ACow+AAeUPhgACYZyWmEEAB8YckA2pUAur3cxqVs3APxmJiCAK5gwFCM4DAA5vhQxSasVTX1jS0F7V0AFLrklRIwTfjCsyKbJabkWdHJW2MVlZ0AlBQ4oEgsHEInwi3wTXaYQQiF6ukmlEB0BgILBDSaH1MYVR4MhRAAdKNTBBejc7g9MawERogA

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

Playground

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