Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript: Use class as interface

I tried to use an Class as Interface for another class. With this I am trying to accomplish an up-to-date mockClass for Testing.

This should be possible and is an favorable approach, as state in https://angular.io/guide/styleguide#interfaces

And is demonstrate here: Export class as interface in Angular2

But strangely I'm getting an Error using Typescript 2.5.3 in VSCode 1.17.2

Error in class SpecialTest

[ts]
Class 'SpecialTest' incorrectly implements interface 'Test'.
  Types have separate declarations of a private property 'name'.

Samplecode:

class Test {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
    }
}

class SpecialTest implements Test {
    private name: string;

    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}

What am I missing?

EDIT: use string instead of String , as @fenton suggested

like image 343
jvoigt Avatar asked Nov 04 '17 18:11

jvoigt


1 Answers

Before we get started, when you extend a class, you use the extends keyword. You extend a class, and implement an interface. There are additional notes on this further down the post.

class SpecialTest extends Test {

Also, watch out for string vs String as this will trip you up. Your type annotations should almost certainly be string (lowercase).

And finally, you don't need to manually assign constructor parameters, so the original:

class Test {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    // ...
}

Is better expressed as:

class Test {
    constructor(private name: string) {
    }

    // ...
}

Now you can choose from a number of solutions to your problem.

Protected Member

Make the name member protected, then you can use it within sub classes:

class Test {
    protected name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
    }
}

class SpecialTest extends Test {
    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}

Interface

This is the solution that I think best matches your needs.

If you pull the public members into an interface, you should be able to treat both classes as that interface (whether or not you explicitly use the implements keyword - TypeScript is structurally typed).

interface SomeTest {
  getName(): string;
  setName(name: string): void;
}

You could explicitly implement it if you wish:

class SpecialTest implements SomeTest {
    private name: string;

    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}

Your code can now depend on the interface rather than on a concrete class.

Using a Class as an Interface

It is technically possible to reference a class as an interface, but there are problems ahead of you do this with implements MyClass.

Firstly, you are adding unnecessary complexity for anyone who has to read your code later, including the future you. You are also using a pattern that means you need to be careful about the keyword. Accidental use of extends may cause a tricky bug in the future when the inherited class is changed. Maintainers will need to become hawkeye over which keyword is used. And all for what? To preserve nominal habits in a structural language.

Interfaces are abstract, and less likely to change. Classes are more concrete, and more likely to change. Using a class as an interface ruins the entire concept of depending on stable abstractions... and instead causes you to depend on unstable concrete classes.

Consider the proliferation of "classes as interfaces" throughout a program. A change to a class (let's say we add a method) can inadvertently cause a change to ripple for vast distances... how many parts of the program now reject input, because the input doesn't contain a method that is not even used?

The better alternatives (when there are no access modifier compatibility issues)...

Create an interface out of the class:

interface MyInterface extends MyClass {
}

Or, just don't reference the original class at all in your second class. Allow the structural type system to check compatibility.

Side note... depending on your TSLint config, weak types (such as an interface with only optional types) will trigger the no-empty-interface-Rule.

Specific Case of Private Members

None of these (using a class as an interface, generating an interface from a class, or structural type) solve the problem of the private member. This is why the solution that solves the real problem is to create an interface with the public members on.

In the specific case of private members, such as in the question, let's think about what will happen if we continue with the original pattern? The natural outcome will be that to preserve the pattern of using the class as an interface, the visibility of members will be changed, like this:

class Test {
    public name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
    }
}

And now we are breaking the more established principles of object-orientation.

like image 88
Fenton Avatar answered Oct 02 '22 19:10

Fenton