Как обратиться к сопоставленному типу с помощью общего индекса в Typescript?

Я пытаюсь получить доступ к свойству из сопоставленного типа, используя универсальный тип. Но я получаю сообщение об ошибке в комментарии ниже. Как правильно напечатать что-то подобное?

type ThingType = 'typeA' | 'typeB';

interface Thing<T extends ThingType> {
    type: T,
    name: string
};

type Container<T extends ThingType> = {
    [id: string]: Thing<T>
}

type Store<T extends ThingType = ThingType> = {
    [K in T]?: Container<K>
};

const myStore: Store = {
    'typeA': {
        'one': {type: 'typeA', name: 'one'},
        'two': {type: 'typeA', name: 'two'}
    }
};

// This one does not fail
const typeAContainer: Container<'typeA'> | undefined = myStore['typeA'];

function storeThing<T extends ThingType>(thing: Thing<T>) {
    // Error here:
    const container: Container<T> | undefined = myStore[thing.type];
    // Type 'Store<ThingType>[T]' is not assignable to type 'Container<T> | undefined'.
    //  Type 'Container<"typeA"> | Container<"typeB"> | undefined' is not assignable to type 'Container<T> | undefined'.
    //    Type 'Container<"typeA">' is not assignable to type 'Container<T>'.
    //      Type '"typeA"' is not assignable to type 'T'.
    //        '"typeA"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ThingType'.
    //          Type 'Store<ThingType>[T]' is not assignable to type 'Container<T>'.
    //            Type 'Container<"typeA"> | Container<"typeB"> | undefined' is not assignable to type 'Container<T>'.
    //              Type 'undefined' is not assignable to type 'Container<T>'.

    // ... code here ...
}

storeThing({type: 'typeA', name: 'three'});

person user3705060    schedule 23.04.2020    source источник


Ответы (1)


На самом деле компилятор не выполняет много высокоуровневого анализа, необходимого для подтверждения присваиваемости типов, которые зависят от еще не определенных параметров универсального типа. Внутри реализации storeThing вы присваиваете значение типа Store<ThingType>[T] переменной типа Container<T> | undefined, где T еще не указан как конкретный тип. И, к сожалению, компилятор не может считать их совместимыми без указания T, даже если они совместимы для всех возможных сужений T.

По сути, это конструктивное ограничение TypeScript. См. microsoft/TypeScript#36737 и microsoft/TypeScript#36349 для подобных проблем такого рода; в обоих случаях компилятор не может следовать взаимосвязи более высокого порядка между универсальным индексированным доступом и другим совместимым типом. Существует открытое предложение microsoft/TypeScript#33014 для обработки подобных случаев. лучше, но неясно, будет ли там что-то реализовано.

До тех пор, пока этого не произойдет, мы должны придумать какой-то путь вперед.


Разумный способ действовать здесь: как только вы убедите себя, что то, что вы делаете, совершенно безопасно (и будьте осторожны; легко ошибиться и подумать, что что-то безопасно, когда это не так), разумное использование утверждение типа подходит:

const container = myStore[thing.type] as Container<T> | undefined; // okay

Такого рода вещи являются предполагаемым вариантом использования утверждений типа: ситуации, в которых вы знаете о типе больше, чем компилятор может проверить. Вы просто утверждаете, что то, что делаете, безопасно, и идете дальше. Конечно, есть некоторая опасность, что вы солгали компилятору... или что ваш код изменится в будущем и превратит ранее правильное утверждение типа в ложь. Утверждения типов переносят бремя проверки безопасности типов с компилятора на разработчика, и это нормально, если вы готовы взять на себя эту ответственность.


Другой способ продолжить: найти некоторые манипуляции с типами, которые достаточно автономны, чтобы компилятор мог следовать вашим рассуждениям даже с неуказанным дженериком. Это немного искусство, и это не всегда возможно. Вот что я бы сделал, это двухэтапный процесс:

const myStoreNarrowed: Store<T> = myStore; // okay
const container: Container<T> | undefined = myStoreNarrowed[thing.type]; // okay

Компилятор может распознать, что безопасно присваивать myStore переменной типа Store<T>, который уже, чем Store<ThingType>. И затем он может распознать, что когда вы индексируете Store<T> с помощью клавиши T, вы получаете что-то, что можно присвоить Container<T> | undefined.

Я бы, вероятно, использовал этот подход, поскольку он по-прежнему дает вам некоторые гарантии безопасности типов, которые теряются при использовании утверждения типа. Но если ничего не помогает, всегда есть (тщательно продуманное) утверждение типа.


Хорошо, надеюсь, это поможет; удачи!

Playground ссылка на код


person jcalz    schedule 24.04.2020
comment
В TS 3.9 есть новая функция, также полезная для этого // @ts-expect-error, вы могли бы упомянуть об этом. - person Akxe; 24.04.2020
comment
Я думаю, что //@ts-expect-error слишком мощен для чего-то, что можно решить с помощью утверждения типа. Мы не пытаемся подавить все возможные ошибки в рассматриваемой строке, а пытаемся устранить одну конкретную ошибку, чтобы компилятор проверил или понял совместимость типов. Если вы сделаете синтаксическую ошибку в этой строке, вы, вероятно, увидите эту ошибку, а не подавите ее. - person jcalz; 24.04.2020