Skip to content

Suggestion: Permit an implementing class to ignore private methods of the implementee class #471

Description

Hi, we have a requirement to create mocks of real classes defined in code. The exact requirements are

  • A specific mock should define the public API of the real class.
  • If the public API of the real class changes in any way then the compiler should detect the corresponding breakage in the mock class.

A possible solution might be as follows:

declare var fileWriter: any; 

// Real class
class Foo {

    public writeToFile(){
        fileWriter.writeToFile('');
    }
}

// Mock
class MockFoo implements Foo {

     public writeToFile(){
         // do nothing
     }
}

This appears to solve the problem because changing Foo.writeToFile will trigger a compilation error along the lines of "Class MockFoo declares interface Foo but does not implement it..."

The problem with this approach is that class Foo is not permitted to have any private methods or fields. If we were to add a private method foo() to class Foo then it's no longer possible for MockFoo to implement Foo because the compiler doesn't permit it.

I suggest that when a class implements another class the implementing class be allowed to ignore the private fields and methods (both static and instance) of the implementee.

(I am aware that there are workarounds, such as declaring an interface that both Foo and MockFoo implement, but that introduces an unnecessary maintenance overhead.)

Activity

  1. danquirk commented on Aug 19, 2014

    @danquirk
    Member

    Is there a reason MockFoo must implement Foo and not extend it?

  2. NoelAbrahams commented on Aug 20, 2014

    @NoelAbrahams
    Author

    Dan Quirk (@danquirk), yes, it would be ideal for MockFoo to extend rather than implement Foo.

    However, that doesn't satisfy the second requirement:

    If the public API of the real class changes in any way then the compiler should detect the corresponding breakage in the mock class.

    To elaborate, suppose we now extend Foo:

    class MockFoo extends Foo {
    
         public writeToFile(){
             // do nothing
         }
    }

    Somewhere else in code, a function that we wish to test:

    function bar(foo: Foo){
        foo.writeToFile();  
    }

    Here's a test case for function bar:

    function test(){
        bar(new MockFoo()); // All good
    }

    Following on from this, if in the next iteration we carry out the following refactor:

    class Foo {
       // Renamed from "writeToFile"
        public writeToFileSync(){
            fileWriter.writeToFile('');
        }
    }

    The compiler does not issue an error for bar(new MockFoo()), and the test case will end up executing the live method.

    Viewed from a different perspective, the compiler isn't able to issue an error, because the deriving class has no mechanism to indicate that it is overriding a base class method. If such a mechanism were to exist then that would also solve this problem.

  3. NoelAbrahams commented on Aug 20, 2014

    @NoelAbrahams
    Author

    A possible solution that occurred to me was to define MockFoo as:

    class MockFoo extends Foo {
    
         public writeToFile(){
             super.writeToFile;
             // do nothing
         }
    }

    Apart from the slightly hackish nature of the solution, there was a problem with this, which I cannot recall...

  4. ivogabe commented on Aug 20, 2014

    @ivogabe
    Contributor

    You could also create an interface IFoo:

    interface IFoo {
        writeToFile(): void;
    }
    class Foo implements IFoo {
        // ...
    }
    class MockFoo implements IFoo {
        // ...
    }
    function bar(foo: IFoo) {
        foo.writeToFile();
    }
  5. NoelAbrahams commented on Aug 20, 2014

    @NoelAbrahams
    Author

    Ivo Gabe de Wolff (@ivogabe), that's a step that we're trying to avoid as I mentioned right at the end of the original post.

    Dan Quirk (@danquirk), it will be interesting to hear the argument against relaxing the need to implement private methods.

  6. RyanCavanaugh commented on Aug 20, 2014

    @RyanCavanaugh
    Member

    The problem is that if you have a private field, MockFoo really is not a Foo:

    class Foo {
        private p = 'hello';
    
        static doSomething(n: Foo) {
            console.log(n.p);
        }
    }
    var  x = new MockFoo();
    Foo.doSomething(x); // Fails

    We don't want to water down the meaning of implements to mean something other than what it is.

  7. NoelAbrahams commented on Aug 20, 2014

    @NoelAbrahams
    Author

    That's a fair point.

    On the other hand, I cannot see any use-cases for the idea of "one class implementing another class", because it's limited to only those classes that don't have any private fields or methods - which are not really common in the real world.

    Perhaps we could invoke this design goal:

    [Not] Apply a sound or "provably correct" type system. Instead, strike a balance between correctness and productivity.

    To me the idea of being able to use the public API of a class as an interfaces is quite appealing; and probably has many interesting use-cases in the real world.

  8. RyanCavanaugh commented on Aug 20, 2014

    @RyanCavanaugh
    Member

    On the other hand, I cannot see any use-cases for the idea of "one class implementing another class", because it's limited to only those classes that don't have any private fields or methods

    This isn't true. The useful example for this is:

    class Animal {
        private something;
    }
    
    class Dog extends Animal {
        woof() {}
    }
    
    // Error, Wolf doesn't have 'woof' method
    class Wolf extends Animal implements Dog {
    }
  9. NoelAbrahams commented on Aug 20, 2014

    @NoelAbrahams
    Author

    I'm not entirely convinced that that example has significantly increased the usefulness of "implementing a class", because now Dog can't have any of its own privates (even though in the real world dogs do have privates 😃 ).

  10. NoelAbrahams commented on Aug 22, 2014

    @NoelAbrahams
    Author

    The other problem that I mentioned with extending the real class was to do with the fact that if the real class had complex constructor parameters then that would be inherited by the mock class. Ideally one would want to simply write var x = new MockFoo() and not have to bother with complex construction. Essentially, we just want MockFoo to respond to the public API of the real class and that behaviour is ideally achieved if we can treat the real Foo as any other interface.

  11. RyanCavanaugh commented on Aug 26, 2014

    @RyanCavanaugh
    Member

    Tagging Suggestion, but understand we're unlikely to make a breaking change here. Other languages use interfaces for this scenario and I think that's still an appropriate solution for TypeScript. We'd need a compelling use case here along with a non-breaking design proposal.

  12. mwisnicki commented on Sep 13, 2014

    @mwisnicki

    Why not simply allow implementing private members of class ?

    class Foo {
      private x = 1;
    }
    class MockFoo implements Foo {
      private x = 1;
    }

    This is less nice than being able to ignore private members but at least you can do mocks and it doesn't break anything.

  13. NoelAbrahams commented on Sep 14, 2014

    @NoelAbrahams
    Author

    Why not simply allow implementing private members of class ?

    Yes, this would be an improvement on the current situation, because as it stands the idea of "implementing another class" is not useful.

  14. 9 remaining items

  15. added
    DeclinedThe issue was declined as something which matches the TypeScript vision
    RevisitAn issue worth coming back to
    and removed on Mar 15, 2016
  16. RyanCavanaugh commented on Mar 15, 2016

    @RyanCavanaugh
    Member

    These are reasonable use cases, but the problem is that we don't want class X implements Y { ... } to produce an X that is not assignable to Y (as would be the case if this worked as proposed).

    If we get a general notion of type operators (many issues wanting this), a simple operator e.g. publicShapeOf could solve these use cases.

  17. Elephant-Vessel commented on Oct 10, 2016

    @Elephant-Vessel

    I'm trying to understand the motivations regarding the language design here. Again I wish to reduce brain pain, hope you don't mind.

    I do understand why privates currently need to be exposed, as the scope of the privates are bound to the class and not the instance. I don't understand, considering the consequences, why the scope of privates are bound to the class and not to the instance.

    Is there any significant value in that a Foo can manipulate the implementation details of any Foo? Something that makes up for the non-intuitiveness and the disturbed ability of the language to easily provide abstractional reusability that comes from mixing in privates with types/interfaces?

    I would like to be able to explain to my colleagues what makes this strange deal with exposed privates worth it after all.

  18. Elephant-Vessel commented on Oct 10, 2016

    @Elephant-Vessel

    Why not simply allow implementing private members of class ?

    Yes, this would be an improvement on the current situation, because as it stands the idea of "implementing another class" is not useful.

    Although that would further complicate this whole concept of privates and what it actually means.

    Same thing with letting subclasses upgrade private and protected fields. Wasn't even aware of that subclasses already can update protected fields, feels like a complete violation of why we have those modifiers in the first place. Feels like this can get messy.

  19. RyanCavanaugh commented on Oct 10, 2016

    @RyanCavanaugh
    Member

    Elephant-Vessel

    I do understand why privates currently need to be exposed, as the scope of the privates are bound to the class and not the instance. I don't understand, considering the consequences, why the scope of privates are bound to the class and not to the instance.

    Certainly TypeScript is not the first language deciding to scope private to the type rather than the instance. The trade-offs are fairly obvious IMO. Probably it's going to be easier to find discussion of the merits of this when applied to Java, C#, C++, etc. rather than us going over that again here.

  20. Elephant-Vessel commented on Oct 10, 2016

    @Elephant-Vessel

    Ryan Cavanaugh (@RyanCavanaugh)

    Certainly TypeScript is not the first language deciding to scope private to the type rather than the instance. The trade-offs are fairly obvious IMO. Probably it's going to be easier to find discussion of the merits of this when applied to Java, C#, C++, etc. rather than us going over that again here.

    Thank you for your response Ryan.

    Sure, but TypeScript differs from those languages as it's structurally typed. And that means that we suddenly have this problem with privates having to be propagated in the type system, a problem that these other languages do not experience.

    I personally believe that solid abstractural integrity is significantly more important than allowing a Foo to mess around with the internals of other Foos. But if the reason for this is that the other languages do it, then I at least know what to say to my colleagues. Brain still not happy, but less pain. Thanks.

  21. kitsonk commented on Oct 10, 2016

    @kitsonk
    Contributor

    I personally believe that solid abstractural integrity is significantly more important than allowing a Foo to mess around with the internals of other Foos.

    I don't think a class having access to/visibility of other instances breaks encapsulation at all. The proposal for ES private properties also includes this class level visibility. It is simply a different form of logical state which the class has access to. I like to think of this private state as a class visible WeakMap, and in fact for Dojo (@dojo) this is actually what we do with our private state, versus using private properties, which ensures at run-time private is really private, as we don't leak our WeakMaps outside of the module the class is defined in.

    The main use case is that the class having access to the private properties allows for static methods to potentially operate on instances without needing to break this encapsulation.

  22. Elephant-Vessel commented on Oct 11, 2016

    @Elephant-Vessel

    Kitson Kelly (@kitsonk)
    Sure, in regards of design constraints the current policy is more a nuisance in regards of reusability than breaking abstractions*. But I think it makes the conceptual model of the language unnecessary convoluted as we now have this unintuitive behavior and the mixup of public contracts with internal implementation within abstractions. How easy wouldn't it have been if we had instance privates instead of class privates, none of this would have been an issue, and the conceptual model would be less complex. May it be that those static methods would have to be instance methods instead.

    * And then we have this stuff about promoting protected members, which I'm really curious about why we got in the first place, and the suggestion about doing the same thing with private members. Talk about breaking encapsulation. For a language whose selling point is to be able to scale, it doesn't feel good at all.

  23. ricklove commented on Nov 22, 2017

    @ricklove

    I like the idea of publicShapeOf where can I track that?

    If we get a general notion of type operators (many issues wanting this), a simple operator e.g. publicShapeOf could solve these use cases.
    From: #471 (comment)

  24. AlexGalays commented on Jan 23, 2018

    @AlexGalays

    If this problem is going to remain, you should at least prevent class from "implementing" other classes, imo.

  25. DanielRosenwasser commented on Apr 17, 2018

    @DanielRosenwasser
    Member

    Since mapped object types strip out private/protected fields (though they also strip out call/construct signatures), Yehuda Katz (@wycats) had the idea of writing the following to achieve this:

    type Interface<T> = {
        [P in keyof T]: T[P]
    }
    
    class C {
        private foo(): string;
    
        bar(): number;
    }
    
    // Look ma, no 'foo'!
    class D implements Interface<C> {
        bar() {
            // ...
        }
    }

    I've also called that mapped object type PropsOf, and you could call it PublicOf if you so desired.

  26. locked and limited conversation to collaborators on Jul 26, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionRevisitAn issue worth coming back toSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions