Путем «добавления опций» вы получаете расширение тип, а не расширение это.Расширение всегда является сужающей операцией (накладывая больше ограничений).Таким образом, вы хотите объединение, а не пересечение ... если вы попытаетесь пересечь два типа без перекрытия, вы получите пустой тип, эквивалентный 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"