#inheritance #typescript-generics #type-safety
#наследование #typescript-дженерики #безопасность типов
Вопрос:
У меня возникли проблемы с объединением дженериков с расширениями интерфейса в typescript. Мой основной вариант использования таков:
- Базовый интерфейс
- Дочерние интерфейсы, которые расширяются от родительского интерфейса (только 1 уровень глубокого наследования)
- Каждый дочерний интерфейс содержит данные, отсутствующие в базовом интерфейсе
- Некоторые поля могут быть или не быть общими для различных родственных интерфейсов
Я хочу иметь возможность написать типобезопасную универсальную функцию, которая может корректно распознавать дочерние интерфейсы в общем виде, включая соответствующие аргументы для универсального интерфейса соответствующим образом.
Я успешно использовал дискриминаторы для расширенных интерфейсов, но у меня возникли проблемы с привязкой его к другим аргументам. Например:
Вот наш базовый тип вместе со значением дискриминатора «Тип»:
interface Base<T> {
type: T
a: string
b: number
}
Вот возможные расширения / наследники:
type ExtensionType = 'CDate' | 'DBool' | 'CString'
interface ExtensionCDate<T = 'CDate'> extends Base<T> {
c: Date
}
interface ExtensionDBool<T = 'DBool'> extends Base<T> {
d: boolean
}
interface ExtensionCString<T = 'CString'> extends Base<T> {
c: string
}
Идея здесь в том, что ExtensionCDate может принимать только общее значение ‘CDate’, и, следовательно, его значение типа всегда равно ‘CDate’ и т. Д.
Вот как я пытаюсь решить эту проблему:
- Используйте тип объединения возможных дочерних интерфейсов:
type GenericExtension<T extends ExtensionType> = ExtensionCDate | ExtensionDBool | ExtensionCString
- Создайте соответствующий интерфейс для каждого дочернего интерфейса, который включает в себя только поля, присутствующие в дочернем интерфейсе, которых НЕТ в базовом интерфейсе (установите разницу по существу):
type ExtendedData<T extends ExtensionType> = Omit<GenericExtension<T>, keyof Base<T>>
- Напишите функцию, которая использует различение для правильной обработки каждого возможного дочернего элемента:
const genericFunc = <T extends ExtensionType>(obj: GenericExtension<T>, data: ExtendedData<T>): void => {
switch ( obj.type ) {
case 'CDate':
obj.c = data.c // data.c should exist
return
case 'DBool':
obj.d = data.d // data.d should exist
return
case 'CString':
obj.c = data.c // data.c should exist
return
}
}
К сожалению, хотя типизация для obj, похоже, работает, дискриминатор также не применяется к параметру data. Я знаю, что мог бы использовать приведение (data.c как ExcludedData<‘CDate’>), но я вижу, что это хакерское решение, а не идеальное.
Ошибка, которую я получаю, заключается в следующем:
TS2339: Property 'c' does not exist on type 'Pick , never>'.
Что заставляет меня думать, что я, по крайней мере, неправильно выполняю тип ExtendedData.
Я чувствую, что решение должно быть довольно простым. Чего мне здесь не хватает?
Спасибо! Дайте мне знать, могу ли я предоставить какой-либо дополнительный контекст, чтобы помочь найти решение.
Ответ №1:
Здесь есть некоторая путаница между дженериком, который зависит от переменного подтипа T
, и экземплярами конкретных подтипов.
type GenericExtension<T extends ExtensionType> = ExtensionCDate | ExtensionDBool | ExtensionCString
В приведенной выше строке вы объявили дженерик T
, но фактически не использовали T
его в своем определении типа.
interface ExtensionCDate<T = 'CDate'> extends Base<T> {
c: Date
}
В конкретных расширениях то, что вы на самом деле сделали, <T = 'CDate'>
это установили строковый литерал CDate
в качестве значения по умолчанию для T
if T
is not set , но на самом деле для него все равно можно установить любое значение, потому что мы не накладывали на него никаких ограничений.
Вы хотите, чтобы ваши конкретные типы расширений больше не принимали универсальную T
переменную. Вместо этого мы вручную устанавливаем T
для Base<T>
.
interface ExtensionCDate extends Base<'CDate'> {
c: Date
}
interface ExtensionDBool extends Base<'DBool'> {
d: boolean
}
interface ExtensionCString extends Base<'CString'> {
c: string
}
Поскольку our GenericExtension
на самом деле не является «универсальным» в смысле typescript, давайте назовем его AnyExtension
вместо этого. Этот тип представляет собой объединение трех конкретных типов расширений.
type AnyExtension = ExtensionCDate | ExtensionDBool | ExtensionCString
На самом деле мы можем вывести объединение трех строк типа из этого, а не записывать его.
type ExtensionType = AnyExtension['type'] // evaluates to type "CDate" | "DBool" | "CString"
Мы все равно столкнемся с некоторыми проблемами с switch
оператором, потому что переключение на основе obj.type
уточняет знания typescript о типе obj
, но не о T. Даже если вы знаете, что если obj.type === "CDate"
это T
также должно быть "CDate"
, typescript не совершает этого скачка и, следовательно, не будет уточнять свои знания о типе data
на основе switch
.
К сожалению, я думаю, вам нужно какое-то as
утверждение, по крайней data
мере, для переменной.
Комментарии:
1. Спасибо за ответ! С тех пор я отказался от того, чтобы заставить это работать, и в итоге выбрал другой маршрут. К сожалению, typescript не позволяет этому работать чисто!