node.js - 如何使用覆盖实现交集类型
问题描述
我正在创建一个控件管理器,它将成为其他控件管理器的抽象基础:ButtonManager、InputManager、PopupManager 等。这些控件有一些相似之处,但不是全部。例如,大小和意图。我想在 ControlManager 中定义共享类型以及在 ControlManager 中使用这些类型的接口。
所有控件都有一个意图,但并非所有控件都具有相同的意图集,因为可以添加基本集。我希望能够在抽象 ControlManager 类中创建基本 ControlIntent 类型并在派生类中扩展它。
我应该注意我将 ControlManager 作为一个抽象类,因为我想强制实现 ControlManager 的类来定义某些功能,例如 setIntentClass、setSizeClass 等
控制管理器将 ControlIntent 类型定义为
export type ControlIntent = 'Default' | 'Disabled'
扩展 ControlManager 的 ButtonManager 然后将其意图类型定义为
export type ButtonIntent = ControlManager.ControlIntent & 'Secondary' | 'Success' | 'Warning' | 'Danger'
ControlManager 中的界面定义了一些共享选项。以意图为例:
export interface IOptions {
controlIntent: ControlIntent
}
然后在 ButtonManager 我想扩展选项接口并覆盖意图属性:
export interface IOptions extends ControlManager.IOptions {
controlIntent: ButtonIntent
}
可能我错过了大局,但在我看来,我应该能够强制我实现的控制管理器具有至少在基类中定义的类型选项的大小和意图。“默认”和“禁用”表示意图,但能够在扩展接口中添加新意图,而无需创建新属性。
总结一下:
所有控件都有一个大小和意图,至少包含一组最少的预定义选项。然后我可以在不同的控制管理器中使用交集来添加到预定义的选项,但希望能够在基本接口中定义所述选项,然后在派生接口中扩展它们。
这是一个实际的设计决策吗?如果是,我该如何完成?非常感谢所有贡献者。
解决方案
通过“添加选项”,您正在做的是扩大类型,而不是扩展它。扩展始终是一种缩小操作(设置更多限制)。所以你想要一个联合,而不是一个交集......如果你试图让两种类型相交而没有重叠,你会得到一个相当于的空类型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
extends IOptions
,那么一个IButtonOptions
对象就是一个IOptions
对象。意思是如果你要一个IOptions
对象,我可以给你一个IButtonOptions
对象,你会很高兴。但是由于您要求一个IOptions
对象,您希望它的controlIntent
属性是'Default'
or '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"
推荐阅读
- java - 如何将字符串转换为 java.sql.Date?
- node.js - 如何使用 PostgreSQL 保存用户
- java - 检查项目中的所有类是否都有特定的注释
- instagram - 嵌入在 HTML 网页中的 Instagram Live Feed?
- pandas - 绘制从时间序列分布在几个月内的每日平均值
- javascript - 如何用 javascript 在浏览器中通过 id 替换任何现有的元素选择?
- maven - SpotBugs 忽略 src 文件夹中的整个目录
- javascript - React Material UI - 列表中的单选按钮都是可选的
- c# - 查询 webApi 时添加了 3 个新标头,但它们来自哪里?
- javascript - 在重叠的行和按钮中隔离单击侦听器