#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>
isnever
, это сводится к justundefined
. Тип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).