首页 > 解决方案 > 如何使用覆盖实现交集类型

问题描述

我正在创建一个控件管理器,它将成为其他控件管理器的抽象基础: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
}

可能我错过了大局,但在我看来,我应该能够强制我实现的控制管理器具有至少在基类中定义的类型选项的大小和意图。“默认”和“禁用”表示意图,但能够在扩展接口中添加新意图,而无需创建新属性。

总结一下:

所有控件都有一个大小和意图,至少包含一组最少的预定义选项。然后我可以在不同的控制管理器中使用交集来添加到预定义的选项,但希望能够在基本接口中定义所述选项,然后在派生接口中扩展它们。

这是一个实际的设计决策吗?如果是,我该如何完成?非常感谢所有贡献者。

标签: node.jstypescriptoop

解决方案


通过“添加选项”,您正在做的是扩大类型,而不是扩展它。扩展始终是一种缩小操作(设置更多限制)。所以你想要一个联合,而不是一个交集......如果你试图让两种类型相交而没有重叠,你会得到一个相当于的空类型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违反了一个重要的替换原则:如果IButtonOptionsextends 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"

推荐阅读