Проблема с расширением / наследованием дженериков с помощью typescript

#inheritance #typescript-generics #type-safety

#наследование #typescript-дженерики #безопасность типов

Вопрос:

У меня возникли проблемы с объединением дженериков с расширениями интерфейса в typescript. Мой основной вариант использования таков:

  1. Базовый интерфейс
  2. Дочерние интерфейсы, которые расширяются от родительского интерфейса (только 1 уровень глубокого наследования)
  3. Каждый дочерний интерфейс содержит данные, отсутствующие в базовом интерфейсе
  4. Некоторые поля могут быть или не быть общими для различных родственных интерфейсов

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

Я успешно использовал дискриминаторы для расширенных интерфейсов, но у меня возникли проблемы с привязкой его к другим аргументам. Например:

Вот наш базовый тип вместе со значением дискриминатора «Тип»:

 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’ и т. Д.

Вот как я пытаюсь решить эту проблему:

  1. Используйте тип объединения возможных дочерних интерфейсов:
 type GenericExtension<T extends ExtensionType> = ExtensionCDate | ExtensionDBool | ExtensionCString
 
  1. Создайте соответствующий интерфейс для каждого дочернего интерфейса, который включает в себя только поля, присутствующие в дочернем интерфейсе, которых НЕТ в базовом интерфейсе (установите разницу по существу):
 type ExtendedData<T extends ExtensionType> = Omit<GenericExtension<T>, keyof Base<T>>
 
  1. Напишите функцию, которая использует различение для правильной обработки каждого возможного дочернего элемента:
 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 не позволяет этому работать чисто!