Похоже на поведение по замыслу или другими словами ограничение конструкции. Такую функцию сложно защитить типом, так как мы можем также передавать подтипы. Например, T extends ObjKeys
охватывает также объединение keyA | keyB
, это означает, что это ограничение не может предполагать, что это будет один из вариантов.
Чтобы полностью установить ограничение, мы можем go использовать следующий подход:
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')
Как видите, я сделал дополнительный параметр, чтобы сузить тип для ключей K1
и K2
. Самая важная строка - { [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } }
. Это означает, что мы имеем дело с типом, который охватывает оба ключа, но все же определение совместимо с нашим типом obj
.
Мы все еще можем использовать obj
в качестве внешней константы, но тогда нам нужно использовать введите утверждение:
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')
Почему это работает? Это работает, потому что мы статически устанавливаем тип с точными типами, которые мы имеем / будем иметь. Если мы говорим T extends A
, это не означает, что T - это A, это также не означает, что T - это некоторый вариант A. Говоря, T extends A
мы сужаем, ограничиваем тип T
для присвоения A
Поэтому мы не строги. Делая тип {[K in T]: X}, T extends Y
, мы строго определяем, что тип будет иметь точный ключ T
, и это будет строго один тип из возможных присваиваемых типов до Y
. Такое строгое определение равно установке значения c типа Obj['keyA']
.