在 TypeScript 中,一个接口可以扩展一个类,这是为了啥?

Posted

技术标签:

【中文标题】在 TypeScript 中,一个接口可以扩展一个类,这是为了啥?【英文标题】:In TypeScript an interface can extend a class, what for?在 TypeScript 中,一个接口可以扩展一个类,这是为了什么? 【发布时间】:2016-12-31 16:24:34 【问题描述】:

在TypeScript Handbook 在“将类用作接口”部分中,有一个扩展类的接口示例。

class Point ... interface Point3d extends Point ...

这在什么时候有用?你有这方面的实际例子吗?

【问题讨论】:

【参考方案1】:

以这个类为例:

class MyClass 
    public num: number;
    public str: string;

    public constructor(num: number, str: string) 
        this.num = num;
        this.str = str;
    

    public fn(arr: any[]): boolean 
        // do something
    

你可以像这样创建一个实例:

let a1 = new MyClass(4, "hey");

但你也可以像这样创建一个满足相同接口的对象:

let a2 = 
    num: 3,
    str: "hey",
    fn: function(arr: any[]): boolean 
        // do something
    

a1instanceof MyClass,而 a2 只是一个对象,但它们都实现了相同的接口。 接口扩展类的意义在于,你可以拿类定义的接口进行扩展。

由于语言的性质,这可能只是一种可能性,但这里有一个可能有用的示例:

class Map<T> 
    private _items:  [key: string]: T ;

    set(key: string, value: T)  ... 

    has(key: string): boolean  ... 

    get(key: string): T  ... 

    remove(key: string): T  ... 


interface NumberMap extends Map<number> 
interface StringMap extends Map<string> 
interface BooleanMap extends Map<boolean> 

function stringsHandler(map: StringMap)  ... 

【讨论】:

【参考方案2】:

如Interfaces section of TypeScript Handbook中所述:

接口甚至继承基类的私有成员和受保护成员。这意味着,当您创建的接口扩展具有私有或受保护成员的类时,该接口类型只能由该类或其子类实现。 p>

这种限制似乎是私有成员和受保护成员继承的副作用。

class Parent

    private m_privateParent;


interface ISomething extends Parent

    doSomething(): void; 


class NoonesChild implements ISomething

/**
 * You will get error here
 * Class 'NoonesChild' incorrectly implements interface 'ISomething'.
 * Property 'm_privateParent' is missing in type 'NoonesChild'
 */
    doSomething()
    
        //do something
    


class NoonesSecondChild implements ISomething

/**
 * Nope, this won't help
 * Class 'NoonesSecondChild' incorrectly implements interface 'ISomething'.
 * Types have separate declarations of a private property 'm_privateParent'.
 */
    private m_privateParent;

    doSomething()
    
        //do something
    


class ParentsChild extends Parent implements ISomething

/**
 * This works fine
 */
    doSomething()
    
        //Do something
    

【讨论】:

【参考方案3】:

我也很难理解“你为什么要这样做?”这是我学到的。

如前所述,接口甚至继承基类的私有成员和受保护成员。这意味着,当您创建一个扩展具有私有或受保护成员的类的接口时,该接口类型只能由该类或其子类实现。

假设您正在为用户界面实现控件。您需要标准控件,例如按钮、文本框和标签。

层次结构是:

class Control
    private state: any;

class Button extends Control

class TextBox extends Control

class Label extends Control

注意 Control 中的私有状态值。这很重要。

现在假设我们想要一种方法来引用可以由某些激活触发的控件。例如,可以单击一个按钮。当您输入文本并按 Enter 时,可以激活文本框。但是,标签是装饰性的,因此用户无法对其进行任何操作。

我们可能想要一种方法来引用此类控件,以便我们可以仅使用这些类型的控件进行操作。例如,我们可能想要一个接受控件作为参数的函数,但我们只想要可以激活的控件。

假设我们尝试用一个扩展类的普通接口来描述这些控件(这是不正确的,但我会稍微解释一下原因)。

// WARNING: This isn't correct - see rest of post for details.
interface ActivatableControl
    activate(): void;

任何实现该接口的东西都可以被视为一个 ActivatableControl,所以让我们更新我们的层次结构:

class Control
    private state: any;

interface ActivatableControl
    activate(): void;

class Button extends Control implements ActivatableControl
    activate()

class TextBox extends Control implements ActivatableControl
    activate()

class Label extends Control

如上所述,Label 没有实现 ActivatableControl。所以一切都很好,对吧?

这就是问题所在 - 我可以添加另一个实现 ActivatableControl 的类:

class Dishwasher implements ActivatableControl
    activate()

界面的目的是用于可以激活的控件,而不是用于无关的对象。

所以我真正想要的是指定一个界面,该界面需要某些控件是可激活的,仅此而已。

为了做到这一点,我让我的界面扩展了 Control,如下所示:

class Control
    private state: any;

interface ActivatableControl extends Control 
    activate(): void;

由于 Control 具有私有值,只有 Control 的子类可以实现 ActivatableControl

现在,如果我尝试这样做:

// Error!
class Dishwasher implements ActivatableControl
    activate()

我会收到 Typescript 错误,因为 Dishwasher 不是控件。

附加说明:如果一个类扩展了Control,并实现了activate,那么它可以被视为一个ActivatableControl。本质上,即使没有显式声明接口,该类也实现了接口。

所以下面的 TextBox 实现仍然让我们把它当作一个 ActivatableControl:

class TextBox extends Control 
    activate()

这是层次结构的最终版本,其中一些代码显示了我可以用它做什么:

class Control 
    private state: any;

interface ActivatableControl extends Control 
    activate(): void;

class Button extends Control implements ActivatableControl 
    activate()  

// Implicitly implements ActivatableControl since it matches the interface and extends Control.
class TextBox extends Control 
    activate()  

class Label extends Control 


// Error - cannot implement ActivatableControl because it isn't a Control
/*
class Dishwasher implements ActivatableControl 
    activate()  

*/
// Error - this won't work either. 
// ActivatableControl extends Control, and therefore contains state as a private member.
// Only descendants of Control can implement ActivatableControl.
/*
class Microwave implements ActivatableControl 
    private state: any;
    activate()  

*/

let button: Button = new Button();
let textBox: TextBox = new TextBox();
let label: Label = new Label();
let activatableControl: ActivatableControl = null;

// I can assign button to activatableControl.
activatableControl = button;

// Same with textBox since textBox fulfills the contract of an ActivatableControl.
activatableControl = textBox;

// Error - label does not implement ActivatableControl
// nor does it fulfill the contract.
//activatableControl = label;

function activator(activatableControl: ActivatableControl)
    // I can assume activate can be called 
    // since ActivatableControl requires that activate is implemented.
    activatableControl.activate();

【讨论】:

【参考方案4】: 限制界面的使用。 如果我们不希望任何类可以实现接口,可以使用此解决方案。 假设接口“I”扩展类“C”。那么 'I' 只能由 'C' 或其子级来实现。 或者如果一个类需要实现“I”,那么它应该首先扩展“C”。

请参阅下面的示例。

// This class is a set of premium features for cars.
class PremiumFeatureSet 
    private cruiseControl: boolean;


// Only through this interface, cars can use premium features.
// This can be 'licensed' by car manufacturers to use premium features !!
interface IAccessPremiumFeatures extends PremiumFeatureSet 
    enablePremiumFeatures(): void


// MyFirstCar cannot implement interface to access premium features.
// Because I had no money to extend MyFirstCar to have PremiumFeatureSet.
// Without feature, what's the use of a way to access them?
// So This won't work.
class MyFirstCar implements IAccessPremiumFeatures 

    enablePremiumFeatures() 

    


// Later I bought a LuxuryCar with (extending) PremiumFeatureSet.
// So I can access features with implementing interface.
// Now MyLuxuryCar has premium features first. So it makes sense to have an interface to access them.
// i.e. To implement IAccessPremiumFeatures, we need have PremiumFeatureSet first.

class MyLuxuryCar extends PremiumFeatureSet implements IAccessPremiumFeatures 
    enablePremiumFeatures() 
        // code to enable features
    

【讨论】:

以上是关于在 TypeScript 中,一个接口可以扩展一个类,这是为了啥?的主要内容,如果未能解决你的问题,请参考以下文章

在 TypeScript 中扩展和定义函数签名

我们如何在 TypeScript 中为非全局接口创建扩展方法?

TypeScript:用自己的类扩展 Express.Session 接口

在 TypeScript 中扩展参数化接口的通用接口

为 TypeScript 扩展控制台接口

如何使泛型 TypeScript 接口从另一个泛型接口扩展?