21

I was exploring the possibility of having a class implementing a class in TypeScript.

Hence, I wrote the following code Playground link:

class A {
    private f() { console.log("f"); }
    public g() { console.log("G"); }
}

class B implements A {
    public g() { console.log("g"); }
}

And I got the error: Class 'B' incorrectly implements class 'A' --- property 'f' is missing in type 'B' coupled with a suggestion that I actually meant extends.

So I tried to make a private field called f (public didn't work as it detects they have different access modifiers) Playground link

Now I get the error: Class 'B' incorrectly implements class 'A'. Types have separate declarations of a private property 'f'; this leaves me very confused:

  • why do private members even matter - if I implement the same algorithm using different data structures, will I have to declare something named the same just for the sake of type checking?
  • why do I get the error when implementing f as a private function?

I wouldn't do this in practice, but I am curious about why TS works like this.

Thanks!

Radu Szasz
  • 981
  • 1
  • 9
  • 22
  • @Igor I don't want that :) I am exploring the way `implements` behaves on classes. – Radu Szasz Feb 23 '18 at 17:52
  • Ok. Does this shed any light on your question? [Extending vs. implementing a pure abstract class in TypeScript](https://stackoverflow.com/a/35990799/1260204). – Igor Feb 23 '18 at 17:54
  • @Igor No, as I already implement all of the functions in the class I'm implementing (see second Playground link) – Radu Szasz Feb 23 '18 at 17:56
  • 1
    You may also be interested in [this issue](https://github.com/Microsoft/TypeScript/issues/471) – CRice Feb 23 '18 at 18:01

4 Answers4

36

The issue Microsoft/TypeScript#18499 discusses why private members are required when determining compatibility. The reason is: class private members are visible to other instances of the same class.

One remark by @RyanCavanaugh is particularly relevant and illuminating:

Allowing the private fields to be missing would be an enormous problem, not some trivial soundness issue. Consider this code:

class Identity { private id: string = "secret agent"; public sameAs(other: Identity) { return this.id.toLowerCase() === other.id.toLowerCase(); } } class MockIdentity implements Identity { public sameAs(other: Identity) { return false; } }
MockIdentity is a public-compatible version of Identity but attempting to use it as one will crash in sameAs when a non-mocked copy interacts with a mocked copy.

Just to be clear, here's where it would fail:

const identity = new Identity();
const mockIdentity = new MockIdentity();
identity.sameAs(mockIdentity); // boom!

So, there are good reasons why you can't do it.


As a workaround, you can pull out just the public properties of a class with a mapped type like this:

type PublicPart<T> = {[K in keyof T]: T[K]}

And then you can have B implement not A but PublicPart<A>:

class A {
    private f() { console.log("f"); }
    public g() { console.log("G"); }
}

// works    
class B implements PublicPart<A> {
    public g() { console.log("g"); }
}

Hope that helps; good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    I think the example is a bit dishonest, `other.id` wouldn't have been accessible in the `sameAs` method, right? – nomadoda Oct 22 '20 at 14:08
  • 1
    If you're suggesting that the `Identity` class as written is invalid, [you can test it yourself](https://tsplay.dev/3NDA8m) and see that it is indeed valid. An instance of a class with a private property can access that property on other instances of the same class. That's how TS's `private` works and also how JS's upcoming `#` private property syntax works (you can test it yourself in Chrome, I think). So rest assured that the examples here are in good faith. Cheers! – jcalz Oct 22 '20 at 16:31
  • 2
    "An instance of a class with a private property can access that property on other instances of the same class" – interesting, I didn't know that. Thanks – nomadoda Oct 23 '20 at 11:32
  • I was thinking the same thing, that interpretation of "private" by the typescript spec strikes me as quite odd. – RocketMan Feb 12 '21 at 21:41
  • 1
    This *private* access is the same in most programming languages. However, ECMAScript (JavaScript) is introducing private class fields (with the `#` prefix) which are actually inaccessible by instances of the same class. A world first? \[[1](https://github.com/tc39/proposal-class-fields)\] \[[2](https://github.com/tc39/proposal-private-methods)\] – Lars Gyrup Brink Nielsen Mar 03 '21 at 07:34
  • @LarsGyrupBrinkNielsen `#`-private fields will act the same way as `private` does in terms of accessibility: an instance of class `Foo` will indeed be able to access private members of other instances of class `Foo`. If your runtime environment supports private fields (I think Chrome does) you can check for yourself [here](https://jsbin.com/xozuyi/edit?js,console). TypeScript's implementation of it also behaves like this; see [here](https://tsplay.dev/WYJrbw). – jcalz Mar 03 '21 at 14:16
5

The current solution with out-of-the-box support from Typescript is simply

class A {
    private f() { console.log("f"); }
    public g() { console.log("G"); }
}

class B implements Pick<A, keyof A> {
    public g() { console.log("g"); }
}

Explanation: keyof A only returns public properties (and methods) of A, and Pick will then down trim A to only its public properties and their respective type.

Eudes
  • 1,521
  • 9
  • 17
2

In this case it is not possible with the current typescript specifications. There is a tracked issue for this but it is closed.

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


See also Extending vs. implementing a pure abstract class in TypeScript

Igor
  • 60,821
  • 10
  • 100
  • 175
  • 1
    Thanks! It seems like TS compiler doesn't allow doing this at the moment. I feel that in this scenario implementing a class should be fully disabled. – Radu Szasz Feb 23 '18 at 18:07
  • @RaduSzasz - correct. Looking at the GitHub issue you are not the first to have come across it and I doubt you will be the last. Hopefully they will address it one way or another (*make it possible or create a better error message*). – Igor Feb 23 '18 at 18:10
1

This is fundamentally due to the fact the visibility of private members are scoped to the type, and not the instance. Meaning that all objects of the type T have access to the privates of other objects of type T.

This is not a problem in nominatively typed languages as all instances of T inherits the implementation of T, but since typescript is structurally typed, it mean that we can not assume that all instances that fulfill T have the implementation of the class that declares type T.

This means that privately scoped members have to be a part of the public contract of the type, otherwise an object of the structural type T could call a non-existing private member of another object with the same structural type.

Being forced to have privates being a part of a public type contract is bad, and could have been avoided by scoping privates to the instance and not the type.

Alex
  • 14,104
  • 11
  • 54
  • 77