zuraff
zuraff

Reputation: 1472

In TypeScript an interface can extend a class, what for?

in TypeScript Handbook in 'Using a class as an interface' section, there is an example of an interface which extends a class.

class Point { ... } interface Point3d extends Point {...}

When can this be useful? Do you have any practical examples of this?

Upvotes: 47

Views: 37207

Answers (4)

HeroSteve
HeroSteve

Reputation: 303

I also struggled with understanding "Why would you do this?" Here's what I learned.

As mentioned before, interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.

Imagine you are implementing controls for a user interface. You want standard controls like button, textbox, and label.

The hierarchy would be:

class Control{
    private state: any;
}
class Button extends Control{
}
class TextBox extends Control{
}
class Label extends Control{
}

Notice the private state value in Control. That will be important.

Now suppose we want a way to refer to controls that can be triggered by some activation. For instance, a button can be clicked. A textbox can be activated when you enter text and hit enter. However, a label is decorative, so a user can't do anything with it.

We might want a way to refer to such controls so we can do something with only those types of controls. For instance, we may want a function that accepts a control as a parameter, but we only want controls that can be activated.

Suppose we try to describe those controls with a plain interface that doesn't extend a class (this isn't correct, but I'll explain why in a bit).

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

Anything implementing the interface can be treated as an ActivatableControl, so let's update our hierarchy:

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{}

As described above, Label doesn't implement ActivatableControl. So all is good, right?

Here's the problem - I can add another class that implements ActivatableControl:

class Dishwasher implements ActivatableControl{
    activate(){}
}

The purpose of the interface was for controls that can be activated, not for unrelated objects.

So what I really want is to specify an interface that requires certain controls to be activatable, and nothing else.

In order to do that, I make my interface extend Control, like this:

class Control{
    private state: any;
}
interface ActivatableControl extends Control {
    activate(): void;
}

Since Control has a private value, only subclasses of Control can implement ActivatableControl.

Now, if I try to do this:

// Error!
class Dishwasher implements ActivatableControl{
    activate(){}
}

I'll get a Typescript error because Dishwasher isn't a Control.

Additional note: If a class extends Control, and implements activate, then it can be treated as an ActivatableControl. Essentially, the class implements the interface even though the interface isn't explicitly declared.

So the following implementation of TextBox still lets us treat it as an ActivatableControl:

class TextBox extends Control {
    activate(){}
}

So here's the final version of the hierarchy, with some code that shows what I can do with it:

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();
}

Upvotes: 15

Jose
Jose

Reputation: 2769

  • to restrict usage of interface.
  • can use this solution if we do not want any class can implement an interface.
  • Suppose interface 'I' extends class 'C'. Then 'I' can be implemented only by 'C' or its children.
  • or if a class needs implement 'I', then it should extend 'C' first.

See an example below.

// 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
    }
}

Upvotes: 4

antonpv
antonpv

Reputation: 937

As described in Interfaces section of TypeScript Handbook:

Interfaces inherit even the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected members, that interface type can only be implemented by that class or a subclass of it.

Such restriction seems to be side effect of private and protected members inheritance.

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
    }
}

Upvotes: 17

Nitzan Tomer
Nitzan Tomer

Reputation: 164307

Take this class for example:

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
    }
}

You can create an instance like so:

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

But you can also create an object that satisfies the same exact interface like so:

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

The a1 is an instanceof MyClass, while a2 is just an object, but they are both implementing the same interface.
The point of interfaces extending classes is exactly that, you can take the interface that the class defines and extend it.

Maybe it's just a possibility due to the nature of the language, but here's an example of where it might be useful:

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) { ... }

Upvotes: 23

Related Questions