Как реализовать тип пересечения с переопределениями - PullRequest
0 голосов
/ 02 мая 2019

Я создаю диспетчер элементов управления, который станет абстрактной основой для других диспетчеров элементов управления: ButtonManager, InputManager, PopupManager и т. Д. Элементы управления имеют некоторое сходство, но не все. Например, размер и намерение. Я хочу определить общие типы в ControlManager, а также интерфейс в ControlManager, который использует эти типы.

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

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

Диспетчер управления определяет тип ControlIntent как

export type ControlIntent = 'Default' | 'Disabled'

ButtonManager, который расширяет ControlManager, затем определяет его тип намерения как

export type ButtonIntent = ControlManager.ControlIntent & 'Secondary' | 'Success' | 'Warning' | 'Danger'

Интерфейс в ControlManager определяет некоторые общие параметры. Используя намерение в качестве примера:

export interface IOptions {
  controlIntent: ControlIntent
}

Затем в ButtonManager я хочу расширить интерфейс параметров и переопределить свойство intent:

export interface IOptions extends ControlManager.IOptions {
  controlIntent: ButtonIntent
}

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

Подведем итог:

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

Это практическое проектное решение, и если да, то как мне его выполнить? Большое спасибо всем авторам.

1 Ответ

1 голос
/ 02 мая 2019

Путем «добавления опций» вы получаете расширение тип, а не расширение это.Расширение всегда является сужающей операцией (накладывая больше ограничений).Таким образом, вы хотите объединение, а не пересечение ... если вы попытаетесь пересечь два типа без перекрытия, вы получите пустой тип, эквивалентный never (иногда компилятор фактически свернет тип до never, а иногда -будет держать пересечение, но вы обнаружите, что не можете присвоить ему какие-либо полезные значения):

type ControlIntent = 'Default' | 'Disabled'

// note the parentheses I added because the operators don't have the precedence you think
type ButtonIntent = ControlIntent & ('Secondary' | 'Success' | 'Warning' | 'Danger') // oops
// check with IntelliSense:  type Button = never

Итак, тип, который вы, вероятно, подразумеваете, таков:

type ControlIntent = 'Default' | 'Disabled'
type ButtonIntent = ControlIntent | ('Secondary' | 'Success' | 'Warning' | 'Danger') 
// type ButtonIntent = "Default" | "Disabled" | "Secondary" | "Success" | "Warning" | "Danger"

Это здорово, но путаница между сужением / расширением / пересечением и расширением / супер / объединением сохраняется в ваших интерфейсах.Следующее определение (я меняю имя на IButtonOptions, чтобы оно могло находиться в том же пространстве имен, что и IOptions) теперь становится ошибкой:

export interface IOptions {
  controlIntent: ControlIntent
}

export interface IButtonOptions extends IOptions { // error!
//               ~~~~~~~~~~~~~~ 
// Interface 'IButtonOptions' incorrectly extends interface 'IOptions'.
  controlIntent: ButtonIntent
}

Это потому, что IButtonOptions нарушает важный принцип подстановки : если IButtonOptions расширяет IOptions, тогда IButtonOptions объект равен IOptions объекту.Это означает, что если вы попросите IOptions объект, я могу дать вам IButtonOptions объект, и вы будете счастливы.Но поскольку вы запросили объект IOptions, вы ожидаете, что его свойство controlIntent будет 'Default' или 'Disabled'.Вы бы по праву были бы недовольны мной, если бы у вашего предполагаемого объекта IOptions было какое-то другое значение для controlIntent.Вы бы посмотрели на это и сказали: «Подождите, что это за строка "Secondary" здесь?


Так что вам нужно изменить дизайн интерфейсов, чтобы это работало. Вам придется отказаться отидея о том, что IButtonOptions является подтипом IOptions. Вместо этого вы можете подумать о создании IOptions универсального типа , в котором тип свойства controlIntent может быть задан с помощью универсального параметра.например:

export interface IOptions<I extends string = never> {
  controlIntent: ControlIntent | I;
}

export interface IButtonOptions extends IOptions<ButtonIntent> {
  // don't even need to specify controlIntent here
}

const bo: IButtonOptions = {
    controlIntent: "Success";
} // okay

Таким образом, параметр I должен быть назначен на string, и он по умолчанию - never, так что тип IOptions без указанного параметратот же тип, что и ваш исходный IOptions.

Но теперь IButtonOptions не расширяет IOptions, а вместо этого расширяет IOptions<ButtonIntent>. Тогда все работает.

Держите вИмейте в виду, что если вы сделаете это, функции, которые раньше ожидали объектный параметр IOptions, теперь также должны стать общими:

function acceptOptionsBroken(options: IOptions) {}
acceptOptionsBroken(bo); // oops, error
//                  ~~ 
// Argument of type 'IButtonOptions' is not assignable to parameter of type 'IOptions<never>'.

Хорошо, надеюсь, это поможет вам продолжить. Удачи!

function acceptOptions<I extends string>(options: IOptions<I>) {}
acceptOptions(bo); // okay, I is inferred as "Secondary" | "Success" | "Warning" | "Danger"
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...