-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Explain why private and protected members in a class affect their compatibility #18499
Comments
Because, at the moment, in ECMAScript, there is no real Consider the following: class Foo {
private _bar = 'bar';
log() {
console.log(this._bar);
}
}
class Baz extends Foo {
private _bar = 'baz';
}
const baz = new Baz();
baz.log(); // if TypeScript allowed it, it would log "baz" which is not what it should be Because all properties and methods are visible at run-time, TypeScript has no choice but to consider those as part of the shape/structure of an instance. |
Hello @kitsonk and thank you for this quick response! Up to here, this is what I had understood. Nevertheless... I would understand that I am not allowed to overwrite a private/protected name, but why force me to implement it when all I want is to implement the same contract (as in unit test stubs). This is exactly the behavior of my workaround using the all-privates-are-optional--patent-pending pattern:
I come from years of rigid typing systems and the refreshing side of languages such as Javascript and Python is this "duck typing" which makes mocking a pleasure (but brings other scary issues). To me Typescript is bringing the best of both worlds... in a kind of magical balance... Here we seem to be losing our balance IMHO. |
It is dealing with the reality that there are no private or projected members. TypeScript could bury its head in the sand and ignore it, or it could face the reality and protect its users. It isn't a TypeScript thing, it is ECMAScript thing. ALL properties are public at run-time, therefore they are part of the structure, therefore in a structural typing system they count as part of the structure. In certain use cases it is much better to create truly private data, using the language: interface PrivateData {
foo?: string;
}
const privateData = new WeakMap<Foo, PrivateData>();
class Foo {
constructor() {
privateData.set(this, {});
}
log() {
console.log(privateData.get(this).foo));
}
}
export default Foo; In this situation, the private data is truly private and does not effect the structure of the instance. The issue to implement the ECMAScript proposal for private fields is tracked at #9950. One can only assume once there are private fields that are truly private, they will not be considered as part of the structure of the instance. |
Well, again this is a matter of balance... why have implemented the private / protected keywords in Typescript if the suggestion is not to use them and to come up with a pattern such as the one you suggest? This pattern is fine to store sensitive data such as an authentication token (to prevent access by a browser extension for example), but you will probably admit that is overkill for the standard usage of the private fields like internal state flag etc... Those two keywords are in the language now and the question is whether they are used to make the most sense out of them or not... When I read the language design goals I have the feeling that the current behavior is trying to be too "sound"... Indeed, even within Typescript code, you can always call Object.getPrototypeOf() and bypass compiler visibility very easily... so trying to prevent mistakes should be preferable to prevent real access. Anyway, I will not bother you any longer with an endless discussion... Either you got my point or at least it just feeds your thoughts (the mocking use case in particular). I already apologize for being so insistent... Whatever the outcome, I guess this point is surprising enough that it deserves a few more lines in the documentation... and this does not change the fact that you guys came to a wonderful balance with this language 🥇 |
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; }
}
|
First to be clear I am not a member of the core team. Some of the comments felt as if you were addressing someone who actually makes decisions.
I suspect, with the ES private members at the stage they are at, the TypeScript team would simply not have implemented them. But classes in TypeScript were implemented several years ago, even when classes in ECMAScript weren't even a certain thing. Private and protected where strong concepts from other classed based languages and were often implemented by convention in JavaScript at that point, usually denoted by an
Agreed, but it feels like you think it is an "opinion" if private and protected members are optional to be considered as part of the structural shape of the object. As Ryan pointed out, it really isn't an opinion. They effect the structural shape and it is more sound for TypeScript to consider them. I don't think it is an opinion, or striking a balance. It is more of an abject fact. |
Hello guys, PS : my pattern of optional private fields is after all not so silly: making |
Side note, if during the lifecycle of a class, a private field might be undefined, I prefer to utilise |
For those who might reach this issue through a search on mocking issues, here I like better pattern than my private-is-optional one described above:
This is the kind of weird believe me! cast, but at least you can do unit testing w/o putting your private fields optional. |
A trick to quickly implement all the public interfaces: class MyClassMock implements MyClass {
} Use the IDE (VSCode) auto implement feature to implement the methods: class MyClassMock implements MyClass {
pubField: number
} And then removing the interface: class MyClassMock {
pubField: number
} Repeat that whenever you have new public properties introduced in |
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; }
} Regarding this, I'm not sure if this is a good example. This seems to me that it pushes the C# feature to TS a bit too far. |
@jandsu regarding You can simply do: You don't have to cast it back to |
This is hardly a C#ism. C++, Java, Swift, etc, all allow cross-instance private property access. See #10516 for discussion on this |
Ok, agree that it is not C#ism. 🌷 btw, nothing against C#, I came from C#. :) |
In JS, all object properties are public 🤔 😉 |
As stated, all object properties are public and in the fields proposal private fields will also be accessible across instance (though it is only alluded to, but it will follow patterns of other languages). Even if you create a private via a What part of JavaScript are you referring to? |
closure function constructor() {
var x = 1
return {
getX() {
return x;
}
}
} |
You get a point for showing that JavaScript can solve things in various ways, but it is still arguable how JavaScript works. This was the more intentional pattern as part of the language prior to ES6: function Foo() { }
Foo.prototype = {
_x: 1,
getX: function () {
return this._x;
}
};
const foo = new Foo(); That I would argue is more JavaScripty... But beauty is in the eye of beholder. |
That method of creating a "class" (with a closure) is not how ES6 classes work at all. It's an entirely different sort of object, one with no sugared equivalent in ES6 or TS |
Yeah, sadly. :) |
I have the same issue: want to mock a type, and I am annoyed by having to include non-public members. class Identity {
private id: string = "secret agent";
public sameAs(other: Identity) {
return this.id.toLowerCase() === other.id.toLowerCase();
}
}
class MockIdentity implements Pick<Identity, 'sameAs'> {
public sameAs(other: Identity) { return false; }
} Would be convenient to have in the language an easy way to "Pick all public member". |
+1 for the "Pick all public member" feature request, something like:
|
@renatomariscal @jandsu type Public<T> = { [P in keyof T]: T[P] } |
I also think it'd be nice to have it behave in the expected way: public interface checked only. From what I can see, as long as TS screams about attempted external private/protected property access then there's no issue, because this is already disallowed anyway? Following up, I'm pretty happy using |
I believe that the type system should require one not to implement any private field from a ParentClass used in But if property characteristics (accessibility levels of properties, etc) could live inside of types and interfaces, then we could selectively choose those parts with special mapped types. For example, It could look like Perhaps a required important limitation of such a feature would be that the accessibility characteristics of properties would only be useful inside of I could really benefit from type/interfaces containing property characteristics (like class types do) in #35416. |
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
This effectively creates strict-nominal types. It's also still causing problems across many projects which have to exactly align library versions to the smallest number in order to make everything build. For example, see microsoft/tsdoc#223 Yarn's strategy is to install the newest possible dependency for each requester, even when installing a slightly older dependency will result with fewer duplicates. This is a sound strategy because the latest version is likely to include patches and security fixes. Solution 1: Get rid of this checkFor those that want stricter "nominal-like" types, they can use a brand instead: class Identity {
' brand' = 'mypackage.mybrand.v1' as const
}
class Identity2 {
' brand' = 'mypackage.mybrand.v2' as const
}
declare let val1: Identity;
let val: Identity2 = val1; resulting with the error:
Solution 2: Remove the private fields when generating declaration filesThere is no need for declaration file consumers to know there are private fields. Cons:
Future solution:
Solution 3: Ask library authors not to pin dependencies to exact versionTBD: research why library authors do this and determine viability. I am in favor of solution 1. TypeScript can still add the private fields in .d.ts files but only check for clashes during inheritance, not assignment. It also makes TypeScript fully align with the structural / branded types strategy which seems to be the most viable option given the current state of the ecosystem |
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
To @spion 's point: using true private fields should remove the need for inclusion in plain objects typed as the class: This is not the case. How do you mock that for tests? |
…ang#737) Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
…ang#737) Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
Sometimes yarn/npm will install different version of protocol-http package under individual clients and root node_modules because of hoisting policy. Private class member will block the ts compiling Reference: microsoft/TypeScript#18499
A problem I've run into here is that there's no way (that I've yet found) to extract private members from a class instance type ( Also, since all members are public at run time, this means that the mapped types are completely wrong if the run-time code is iterating over all members. |
@victortwc Mocking without extending isn't possible because all private/protected properties makes them incompatible with anything that tries to implement the same interface (including true privates), so these are not compatible: class A {
#pri = 1;
}
class B {
#pri = 1;
} This idea is that since a class might try to access another instance's private properties, they aren't necessarily the same. Helpful in those cases, but restrictive in other cases. The only options are:
IMO it would be nice to have an option to mark true private properties 'internal' or 'instance' accessible only, perhaps somehow checked to ensure only being read via |
TypeScript Version: up to 2.5.2 at least
In the doc a short paragraph explains that private and protected members in a class affect their compatibility.
I have been searching for a while in the design goals, on SO etc... but could not find a decent explanation of the rationale. Could we:
Note: I found one rationale close to what I am looking for, but you could imagine that the language "reserves" the private names but does not compel to use them.
Code
This behavior is especially an issue in unit testing where you want to mock a class, disregarding its internals...
Expected behavior:
I would like to only care about the public contract that a class-under-test can possibly use from my mock.
Actual behavior:
I cannot limit my mock to the public fields.
The ugly workaround I found is the following:
What I do not like is that I have to modify the semantics of my class to be able to mock it and that I am scared that a future overzealous refactoring may use the shortcut notation for fields declaration in the constructor... making the constructor param optional (!)
The text was updated successfully, but these errors were encountered: