Установка общего типизированного необязательного сопоставления с объектом со строго типизированным полем

#typescript #typescript-generics

#typescript #typescript-generics

Вопрос:

Я пытаюсь написать код для регистрации загрузчиков строго типизированных данных, однако у меня возникли проблемы с typescript, чтобы правильно настроить карту. В приведенном ниже примере M это карта служб и k список служб с типом поля, который является определенным значением E . Однако, когда я пытаюсь присвоить экземпляр карте, выдается сообщение об ошибке, в котором говорится, что я пытаюсь присвоить значение undefined . Не уверен, куда идти дальше.

 enum E {
  A = 'a',
  B = 'b',
}

interface I<T extends E> {
  type: T;
}

type J<T> = T extends E ? I<T> : never;
export type K = J<E>;

const M: { [T in E]?: I<T> } = {};
const k: K[] = [];

k.forEach(
  <T extends E>(i: I<T>) => {
    M[i.type] = i;
    // ERROR
    // Type 'I<T>' is not assignable to type '{ a?: I<E.A> | undefined; b?: I<E.B> | undefined; }[T]'.
    //  Type 'I<T>' is not assignable to type 'undefined'.
  });
 

Комментарии:

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

Ответ №1:

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

  • microsoft / TypeScript # 27808 (поддержка extends_oneof общего ограничения)) и microsoft / TypeScript # 25879 (поддержка общих индексов в неродовых сопоставленных типах, именно ваш вариант использования выше):

    В настоящее время нет способа сказать, что параметр типа, ограниченный объединением (как ваш T extends E выше), может одновременно использовать только один элемент объединения. Прямо сейчас T extends E это означает, что T это может быть указано с любым из четырех следующих типов: never , E.A , E.B , или E.A | E.B (что просто E ). Если бы можно было сообщить компилятору, что T это может быть E.A , или это может быть E.B , но это не может быть их объединением, тогда, возможно, компилятор мог бы понять, что ваше назначение работает для каждого из них, и принять их оба.

    Но это не то, что происходит. T может быть указано с E.A | E.B помощью , и поэтому M[i.type] = i должно работать в этом случае. Поэтому ваша общая функция обратного вызова также может быть изменена на ее не общую версию

     k.forEach((i: I<E>) => { M[i.type] = i; }); // error!
    // --------------------> ~~~~~~~~~
    // Type 'I<E>' is not assignable to type 'undefined'
     

    за все хорошее, что он делает. Эта версия также содержит ошибки по другим причинам:

  • microsoft / TypeScript#30581 (поддержка коррелированных типов), microsoft / TypeScript #25051 (поддержка анализа потока управления распределением):

    Давайте предположим, что i это относится к типу I<E.A> | I<E.B> . К сожалению, компилятор не может выполнить рассуждения более высокого порядка, необходимые для того, чтобы убедиться, что M[i.type] = i это приемлемо. В левой части присваивания M[i.type] отображается тип M[E.A] | M[E.B] . Но поскольку вы пытаетесь присвоить значение этому типу, компилятор требует, чтобы присвоенное значение имело пересечение этих типов, а не объединение. Это было введено в TypeScript 3.5 через microsoft / TypeScript #30769 как способ повышения безопасности. Итак M[i.type] , с левой стороны отображается как тип M[E.A] amp; M[E.B] , который есть (I<E.A> | undefined) amp; (I<E.B> | undefined) . Поскольку I<E.A> amp; I<E.B> is never , это сводится к just undefined . Тип i справа — I<E.A> | I<E.B> , который не может быть присвоен undefined , и поэтому возникает ошибка.

    Эта ошибка имела бы смысл, если бы, например, у нас было два значения, i и j , оба типа I<E.A> | I<E.B> и пытались присвоить M[i.type] = j ; . Компилятор по праву будет жаловаться, что поскольку j не может быть и I<E.A> того, и I<E.B> другого, то назначать его небезопасно M[i.type] , потому что, возможно i.type !== j.type . Поскольку компилятор обращает внимание только на типы в явно безопасных M[i.type] = i , он в настоящее время не может определить разницу между этим и явно небезопасным M[i.type] = j .

    Что мы хотели бы сообщить компилятору, так это то, что типы M[i.type] и i коррелируют друг с другом: даже если они оба являются типами объединения, невозможно M[i.type] , чтобы быть типа I<E.A> , в то время i как имеет тип I<E.B> . Но в настоящее время это невозможно.


Итак, что вы можете сделать в качестве обходного пути? Самое простое, что вы можете сделать (и, вероятно, правильное, что нужно сделать), это использовать утверждение типа. Вы знаете, что то, что вы делаете, безопасно, поэтому просто скажите компилятору, чтобы он не беспокоился об этом. Самое простое утверждение — бросить any туда гранату:

 k.forEach(
  (i) => {
    M[i.type] = i as any;
  });
 

Есть несколько менее опасных утверждений, которые вы можете использовать:

 k.forEach(
  <T extends E>(i: I<T>) => {
    M[i.type] = i as any as (typeof M)[T];
  });
 

где вы сохраняете общий обратный вызов и сообщаете компилятору, что назначение безопасно, или:

 k.forEach(
  <T extends E>(i: I<T>) => {
    (M as { [K in T]?: I<K> })[i.type] = i;
  });
 

где вы сохраняете общий обратный вызов и сообщаете компилятору, что M его можно обрабатывать так, как если бы у него был только ключ типа T . Этот, вероятно, мой любимый, потому что он ближе всего подходит к выражению того, что вы пытаетесь сделать.

Другим возможным обходным путем является явное сужение i до I<E.A> и I<E.B> , в свою очередь, после чего анализ потока управления компилятора может взять верх:

 k.forEach(i => i.type === E.A ?
  M[i.type] = i :
  M[i.type] = i
)
 

очевидно, что это избыточно, а не то, что вы хотели бы сделать. Но это показывает, что компилятор в принципе мог бы понять, что M[i.type] = i это безопасно, если бы можно было указать компилятору притвориться, что такое перечисление случаев было выполнено (как в microsoft / TypeScript #25051).

Игровая площадка ссылка на код