microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.01k stars 12.29k forks source link

The future of the "private" keyword #31670

Closed G-Rath closed 3 months ago

G-Rath commented 5 years ago

This is a somewhat "catch-all" issue to serve as a place of discussion for a question that is sure to come up given the advancement of Private-Named Instance Fields:


What is the future of the "private" keyword in TypeScript?

That's the general question - following are some short-and-sweet Q&As that I've pulled up for visibility.

Is TypeScript going to depreciate it, and aim towards phasing it out in favor of the private field operator?

Our current plan is to leave the current private behavior as-is.

Is it going to be supported in the form of a transformer , turning private bar into #bar?

definitely "no" because that would require type-directed emit.

A few more questions:

Would #-fields allow modifiers like readonly?

Yes, readonly is meaningful inside the class

Will public #x, private #x and protected #x be an error?

Yes

Will it be possible to assign #-fields from the constructor parameters, the same way as the current parameter-properties work (i.e., constructor (private x) and constructor (#x))?

No

What visibility rules will apply between private entities and #-entities? What if I use a private-entity from a #-entity, or vice versa? Seems that #-entites are 'more' private than private-entities, in some sense. :)

The visibility of a method doesn't change which properties it's allowed to access

Would it be possible to do the following:

class Foo {
    bar: string;
    #baz: number;
}
const foo: Foo = { bar: 'bar' };

No. The given object is not a substitute for Foo; the same reasoning about cross-member access requiring private members to be present applies here

AnyhowStep commented 5 years ago

I would personally prefer to keep the private keyword. To me, it looks waywayway better than #. And is more clear in its intent, to me. And I wouldn't want to have to go back and do a regex replace for private to #.

When I found out # was being proposed instead of just private, I thought I was experiencing the Mandela effect.

RyanCavanaugh commented 5 years ago

Our current plan is to leave the current private behavior as-is.

Reasons for this:

As for a transformer, probably not? It's simple to replace these with a regex if you're motivated.

RyanCavanaugh commented 5 years ago

Oh the other interpretation of "transformer" would be "Would TS emit private as #", the answer to which is definitely "no" because that would require type-directed emit.

fatcerberus commented 5 years ago

FWIW I don’t find the type-directed emit argument that strong in this case; it’s a stretch to call private “type info” IMO (also we’re emitting the class itself, so we could just say it’s an alias for # and thus not really type info at all). That said, changing TS to emit private members as # would be a massive, massive breaking change (at runtime too!), so that’s plenty good reason to avoid going that route as far as I’m concerned.

waywayway

I found this way funnier than I should have, I think.

fatcerberus commented 5 years ago

When I found out # was being proposed instead of just private, I thought I was experiencing the Mandela effect.

On a related note, it’s good to know I’m not the only person who thought I had been dropped into Bizarro World when I first found out about the # sigil.

RyanCavanaugh commented 5 years ago

FWIW I don’t find the type-directed emit argument that strong in this case; it’s a stretch to call private “type info” IMO (also we’re emitting the class itself, so we could just say it’s an alias for # and thus not really type info at all).

Remember that private fields allow cross-instance access. Consider something like this:

class A {
  private y = 0;

  method(arg: A) {
    console.log(arg.y);
  }
}

The "correct" #-based emit of this would require type information on arg in order to detect that y should be rewritten to #y

fatcerberus commented 5 years ago

Yeah, I see what you mean now, basically obj.foo is ambiguous without type info if there’s a possibility it can “really mean” obj.#foo. Thanks, hadn’t thought of the cross-instance case.

mheiber commented 5 years ago

Other sometimes-advantages of OG private is that private fields show up in JSON.stingify output and can show up in console.log.

gsathya commented 5 years ago

Hi I was pointed to this thread by folks regarding the runtime performance concerns mentioned in https://github.com/microsoft/TypeScript/issues/31670#issuecomment-497370201. (For some background, I implemented private fields in V8)

private fields have much better runtime perf, especially in downlevel scenarios

Can you expand on this? My understanding is that typescript private is downleved to public property access. AFAIK, the extra overhead for ES private is just one machine load for loading the PrivateName backing the property when compared to public property access. I don't expect this to be of significant runtime overhead. Is there something else?

zhuravlikjb commented 5 years ago

A few more questions that weren't asked yet (it seems):

  1. Would #-fields allow modifiers like readonly?
  2. Will public #x, private #x and protected #x be an error?
  3. Will it be possible to assign #-fields from the constructor parameters, the same way as the current parameter-properties work (i.e., constructor (private x) and constructor (#x))?
  4. What visibility rules will apply between private entities and #-entities? What if I use a private-entity from a #-entity, or vice versa? Seems that #-entites are 'more' private than private-entities, in some sense. :) Thanks.
nicojs commented 5 years ago
  1. Would it be possible to do this?
    class Foo {
    bar: string;
    #baz: number;
    }
    const foo: Foo = { bar: 'bar' };

    or is #baz part of the shape of Foo? (hope ~not~ so).

RyanCavanaugh commented 5 years ago

AFAIK, the extra overhead for ES private is just one machine load for loading the PrivateName backing the property when compared to public property access. I don't expect this to be of significant runtime overhead. Is there something else?

I over-extrapolated; I'll trust your assessment on this. I've updated the comment to be more accurate.

RyanCavanaugh commented 5 years ago

Would #-fields allow modifiers like readonly?

Yes, readonly is meaningful inside the class

Will public #x, private #x and protected #x be an error?

Yes

Will it be possible to assign #-fields from the constructor parameters, the same way as the current parameter-properties work (i.e., constructor (private x) and constructor (#x))?

No

What visibility rules will apply between private entities and #-entities? What if I use a private-entity from a #-entity, or vice versa? Seems that #-entites are 'more' private than private-entities, in some sense. :)

The visibility of a method doesn't change which properties it's allowed to access

Would it be possible to do this? (structurally skip a # member)

No. The given object is not a substitute for Foo; the same reasoning about cross-member access requiring private members to be present applies here

fatcerberus commented 5 years ago

The given object is not a substitute for Foo; the same reasoning about cross-member access requiring private members to be present applies here

I’m not sure I understand the rationale of this; for TS-private this makes sense because of JS interop (the “private” property is actually public from JS POV); for # fields, only the class itself can access it, even at runtime, and therefore as long as the rest of the shape matches, it should be fine from a duck-typing POV.

Is there a case I’m missing where the above substitution wouldn’t be safe?

edit: Wait... I bet it’s the cross-instance case again, right?

ljharb commented 5 years ago

Private fields seem like they’d require a nominal, not structural, type, since they’re like a brand.

RyanCavanaugh commented 5 years ago

private fields already create a nominal brand; # fields will too

RyanCavanaugh commented 5 years ago

Just to clarify the cross-instance thing:

class Foo {
    bar: string = '';
    #baz: number = 0;
    check(other: Foo) {
        console.log(other.#baz);
    }
}
const foo: Foo = { bar: 'bar', check: (other: Foo) => undefined };
const realFoo = new Foo();
// Runtime error
realFoo.check(other);
trusktr commented 5 years ago

That shouldn't be allowed, right?

fatcerberus commented 5 years ago

No, that’s totally normal in most OO languages in my experience - a class can freely access private members of other instances of itself. Same goes for the proposed # class field syntax.

hax commented 5 years ago

As for a transformer, probably not? It's simple to replace these with a regex if you're motivated.

@RyanCavanaugh Though it seems easy to replace TS private _foo to #foo or vice versa, it actually have many semantic differences and very likely cause many bugs/breaking changes in the whole ecosystem. I suggest TS would give caveat to warn the programmers when land private fields: it will be very dangerous to change private _foo to #foo naively in current code base.


Then some thought about private and #:

I understand why TS team decide to support both private foo and #foo, but to be honest, it will cause more and more problems in long term to have two different private mechanism. Every newcomers will wonder which one I should use, every team need to decide which one should we adopt...

There would at least three options:

  1. only use TS private
  2. only use #foo
  3. use both

When some programmers/teams with difference preference work together there would be arguments, again and again.

Note, TS private and #foo have differences, there may be some use cases which is only possible or much better in one mechanism, but in most use cases, from the engineering viewpoint, there is no much difference. So the benefits of two mechanism never meet their cost.

Such arguments would be a bit like the holy war of Tab vs Space, both side have valid reasons but in the engineering viewpoint they are trivial. Unfortunately, not like tab/space which can be converted seamlessly, private foo and #foo can not be converted easily as my explanation before. This means the decision would never be trivial.

I hope TS team could have a long-term view about this issue. We should not force the programmers and the whole ecosystem to take the cost of two mechanism.

I understand the current situation is largely caused by TC39. The design of TypeScript, as the downstream of the design of ECMAScript, is forced to face such dilemma (not only private field, but also potential big breaking changes due to public fields). As class fields proposal already stage 3, it's very impossible to ask TC39 guys to revisit the issues. But I always think it's wrong to transfer the ten people cost of committee to millions people cost of the whole community.

rjgotten commented 5 years ago

We should not force the programmers and the whole ecosystem to take the cost of two mechanism.

As long as Microsoft's own tooling uses TypeScript-based language services also for regular JS, you don't really have the luxury of only supporting one.

If you want to settle on using only one, then the best case scenario is to support only one at a time. That is: private foo only in .ts files and #foo only in .js files.

Anything else is either going to piss off TS developers or JS developers.

jimbuck commented 5 years ago

I love the fact that TypeScript's private can still be accessed for situations like unit testing. For internal classes I have no reason to use the JS # sigil. On the other hand, for externally accessible classes I see where the # sigil provides value, by protecting users of your library from accessing internal fields.

So as annoying as it may seem, I'm glad it does not conflict with existing TypeScript syntax. I believe that TypeScript should continue to maintain "soft private" via private x and "hard private" via JS's #x.

ljharb commented 5 years ago

Private things aren’t supposed to be tested; you can use symbols for “soft private”.

jimbuck commented 5 years ago

@ljharb To be more clear, I don't mean directly testing private "things", I'm talking about unit testing public things (methods). There is a huge benefit in having access to private fields, it allows for easy mocking and assertions. Using Symbols can provide a similar experience, but I'd rather not have to change paradigms just to maintain functionality supported by the language since the very beginning.

To that end, both approaches (TS's private x and JS's #x) should be supported as they are both provide important functionality.

AnyhowStep commented 5 years ago

Symbols created with Symbol() start to break when you have multiple copies of the same dependencies at different versions. Copy # 1's const privateField = Symbol() is different from Copy # 2's const privateField = Symbol()

The same when using instanceof tests on classes where you have multiple copies of the same package. Copy # 1's class A is different from copy # 2's class A.

It's possible to pass an instance of something from Copy # 1 to copy # 2 and then stuff breaks during run-time.

I wonder if this private thing will also break this way...

ljharb commented 5 years ago

@AnyhowStep yes, it will - different instances will have different private fields, just like as if a closed-over WeakMap was being used.

fatcerberus commented 5 years ago

That kind of thing is exactly why Array.isArray was invented, IIRC.

mbrowne commented 5 years ago

Has there been any discussion about a compiler option that would let the user choose how TypeScript's private should be transpiled? I'm sure some people are fine with only compile-time private, whereas others might want hard private (via #, transpiled to WeakMaps in the meantime) or soft private (via symbols). Given that private is already baked into TypeScript, IMO it might as well do something useful and not be so easily circumvented at run-time. OTOH, I can see the argument for treating private as a legacy feature and always having it be a compile-time only feature, since we will have # also...

mbrowne commented 5 years ago

One correction/clarification: I am aware that TypeScript will support private fields (#), so personally I would not want to instruct the compiler to transpile private to #. Seems much less confusing and error-prone to just use # directly. But I tried to include what others might want in my above assessment. The option to transpile private to symbols is my main suggestion.

RyanCavanaugh commented 5 years ago

@mbrowne We don't have any plans to provide that as a compile step. You can already write your code that way with good typechecking via unique symbol if that's what you'd like to happen at runtime. The presence of # doesn't really change anything either way there.

mbrowne commented 5 years ago

The private modifier is a significantly more concise way to define properties than declaring symbols manually. And while I get your point that # doesn't change anything technically, I think the introduction of # presents an opportunity to create an option for private to do something else, since ideally people will start using # for fields that should truly be private and private would just become a largely unused legacy feature.

However, my suggestion could easily be implemented as a 3rd party Babel plugin instead. It also depends on whether or not TC39 will pursue some other shorthand syntax for symbols, in which case it would make more sense to support that proposal (if someone created it) rather than working on a way for private to transpile to symbols.

rjgotten commented 5 years ago

Symbols created with Symbol() start to break when you have multiple copies of the same dependencies at different versions. Copy # 1's const privateField = Symbol() is different from Copy # 2's const privateField = Symbol()

@AnyhowStep And that's why Symbol.for and Symbol.keyFor exist. If you have pseudo-private members that you need to be able to access across boundaries, they're what you use. (Either that or you export one shared Symbol() from a common module that remains stable.)

Sure, that's not really private anymore: but then, even an anonymous closed-over Symbol() never had guarantees of being private, because you could always access symbols using Object.getOwnPropertySymbols anyway. Quite analogous to how reflection facilities in languages such as C# also still allow you to pull up private class members.

yes, it will - different instances will have different private fields.

@ljharb No, it won't. Different classes will have different private fields. Different instances of the same class will have shared access to private fields of that class.

jimbuck commented 5 years ago

@rjgotten I think @ljharb means multiple instances of the same module at different versions (dependencies of dependencies). They won't be considered the same class, therefore you won't be able to access the private members between versions. Theoretically.

rjgotten commented 5 years ago

@jimbuck I suppose that's possible. It's a bit of a wash what was meant by 'instance' then. (Bit of a loaded term in the context of classes as well.)

ljharb commented 5 years ago

@rjgotten the question was about different classes, but your clarification is correct.

jimbuck commented 5 years ago

@rjgotten Definitely a loaded term, maybe "module instance" is better. But I think the situation that @AnyhowStep describes is definitely possible, has there been any deeper discussion on that?

RyanCavanaugh commented 5 years ago

But I think the situation that @AnyhowStep describes is definitely possible, has there been any deeper discussion on that?

It's a feature, not a bug (seriously)

jimbuck commented 5 years ago

@RyanCavanaugh Hey, as long as there is awareness that different module instances (two versions of the same module) means separate classes and therefore no private access. Module resolution/version alignment will be far more important once # is actively used.

matthewjh commented 4 years ago

@RyanCavanaugh

Will it be possible to assign #-fields from the constructor parameters, the same way as the current parameter-properties work (i.e., constructor (private x) and constructor (#x))?

No

Ryan, I kindly ask you to reconsider this. A lot of codebases have a significant number of classes making heavy use of private but also the constructor assignment shorthand. If those developers want to migrate over to hard-private fields, they will not be able to do so with a simple regex: a regex would easily cover private x: string but not the constructor assignment. I understand retaining both soft- and hard-privates, but it has to be easy to transition soft- to hard-privates, as you alluded to earlier re. using a regex. But with the above it's not going to be easy or possible with a simple regex at all. Frankly, this is going to be a royal pain to migrate to if constructor assignment shorthand isn't extended to support hard-privates. In code that uses constructor dependency injection (Angular comes to mind), for example, this shorthand is used in a majority of classes and the reality is that the decision above is going to prevent migration and cause frustration and confusion.

Also, I think it adds further incongruity. So TypeScript has this shorthand to create fields, and it can be used for public, protected, and private fields, but not for hard-private? That's not consistent. I understand the rationale behind not deviating from the ES specced syntax, but TypeScript already has the constructor shorthand whether people like it or not. It is already being used, whether people like it or not. Not extending this existing feature to a new type of field, given that it works for every other type of field, would be a big mistake, in my opinion, and make the language less intuitive.

robpalme commented 4 years ago

Thanks for highlighting the lack of parameter properties for #private names, @matthewjh.

it has to be easy to transition soft- to hard-privates

On this specific point, it's worth noting that in the general case, migrating a codebase from soft-private to hard-private is unsafe, so any automated conversion would be hazardous. This is because client code might be depending on those properties in all sorts of ways, e.g. for-in iteration, proxies, intentional access that casts away type safety, JS code that didn't realize there was any type safety to begin with... and more ways. The cat is already out of the bag. So my personal expectation is that hard-private will be used more for fresh code rather than refactoring existing code.

matthewjh commented 4 years ago

Thank you @robpalme. I'm not sure I agree with your expectation. In our company codebase, which is all-TS and quite tightly controlled, there are no "unsafe" accesses of soft-private properties. We used soft-privates not because we wanted soft privacy but because it was the only option at the time. We will certainly move to hard privates once they are available.

In my view having a codebase where hard-privates are used for new code and soft-privates for existing code, i.e. an inconsistent mixture of the two based on creation date, rather than on intent and semantics, is something few developers would be happy with... I think more people will want to migrate than you think -- but that is just my opinion. And migration would be fairly easy in codebases like ours if hard-privates can be used in all the positions that soft-privates can (i.e. constructor arguments).

kokushkin commented 4 years ago

Honestly, I don't like # fields. But what I don't like even more is living with two private fields: private and #. If TypeScript is a superset of Javascript then "private" must be deprecated and deleted from the language in the 4th version. All these benefits which were mentioned above don't worth keeping two private fields. It's too confusing for people who use the language. The people for whom language was really created for. The majority of them. The language is not for creators of the language only!

mbrowne commented 4 years ago

@kokushkin I agree with the principle of what you're saying about making the language more user-friendly. If the old private fields truly became a legacy feature, they could be deprecated with a warning and then later on made available only via a special flag, if at all. But that's assuming that they're no longer needed, when in reality I think properties that are private only at compile time is a feature a lot of people would want to keep...so there would have to be a new syntax to declare such fields. And if the syntax for that still used #, it might still be confusing since #foo by itself indicates a field foo that's fully private at run-time.

There has been some discussion in the past at https://github.com/tc39/proposal-class-fields and https://github.com/tc39/proposal-decorators about using decorators to enable "soft private", i.e. fields that can still be accessed somehow outside the class (for the purpose of reflection, etc.). For example, there could be a reflect or expose decorator:

class Demo {
  @reflect
  foo = 1
}

The idea is that the field would still be "private" in the sense that you couldn't access it directly, but it would still be available via some other reflection method, e.g. Object.getOwnPropertySymbols() or maybe a new API. (If it used symbols, then alternatively the decorator could just be called @symbol.)

I think this is something that the decorators proposal needs to figure out, and then we can figure out what hard private / soft private features make the most sense for TypeScript in the future.

kokushkin commented 4 years ago

Thank you, @mbrowne . It seems to be reasonable using decorators for these types of things. At the same time the more people see these two private definitions the more people will be confused. And they are already on the "classes" page of the Handbook! When I think they should be somewhere in a "deep dive" or smth until everything will be ready to make them primary. But before that, just think about how it would look like to having all this together with private and public methods of classes. Doesn't it seem to be awkward? image I mean using # for private fields and "private" for private methods together!

kokushkin commented 4 years ago

I think I've got a little bit more holistic point of view by now. My statement is... if you want to keep your language as a superset of another language and at the same time keep it consistent - you have to make more breaking changes. Because, often, the underlying language changes in a way which not consistent with superstructures of the superset language. Here are possible ways...

Untitled As for me, I would prefer the way II. And if we go the way number I, it would be great to keep the patch away from people's eyes.

mbrowne commented 4 years ago

There is an ECMAScript proposal for private methods that's already in stage 3: https://github.com/tc39/proposal-private-methods#private-methods-and-fields

I don't know what is the status of implementing this in TS, but I assume it will happen, at which point it will be possible to use # for both private fields and methods.

hax commented 4 years ago

There will be lot of pains when use # and private together, this is one of the important reason why we 360 tech group as ECMA member already make the decision of vote for NO on class fields proposal in future meetings if the champions want to advance class fields to stage 4. Unfortunately, class fields proposal already achieve stage 3 several years ago before we join ECMA and TC39. And implementers (include TypeScript) have their own decisions to implement any stage 3 proposals. We will see the consequences soon.

mbrowne commented 4 years ago

Thanks for the update @hax. I am glad that you and the 360 tech group are joining the TC39 committee and adding to the diversity of opinion there. But it sounds like you are also introducing bias if you have already made a decision to vote against advancing class fields to stage 4. This decision should be based in large part on feedback from people who have actually used private fields in production (and consuming libraries that use them). I don't think there's been enough of a data collection period yet to definitively conclude that rolling back class fields is justified. Unless there is real-world data showing that the new syntax is so confusing to people that it harms productivity and adoption, then it's premature to conclude that.

And aside from which issues are anticipated and which issues actually become significant among real-world users, all of this has to be weighed against alternative proposals and tradeoffs—for example, the fact that private foo syntax would make it impossible to achieve true run-time privacy (i.e. hard privacy). The point I was making about TypeScript is that they are merely following the proposed standard, and rejecting the standard to go in their own direction would have negative consequences. Of course the other side of the argument is that implementing private fields in TS was premature, which I disagree with even in the very unlikely event that the class fields proposal is dropped.

RyanCavanaugh commented 4 years ago

I think people are overthinking this. Every programming language has multiple ways of accomplishing the same endgoal with different trade-offs, even down to trivia like how you write the property keys in object literals (single quotes? double quotes? no quotes? computed string name?). # has its own problems and benefits, and private has its own problems and benefits. This is a boon, not a burden.

If you want to work hard to try to confuse yourself, sure, mix # and private in the same class. But if your goal was to write confusing code, there are much better options available to you already, and I don't think this helps you much.

If you don't want to actively sabotage your own codebase, pick one or the other, both will probably be fine in your application, and you can do some light reading ahead of time if you want to understand the difference.

kokushkin commented 4 years ago

What do you think, how many people will use it # feature in day to day practice? If the majority of them then it's not ready, because it's in dissonance with all other "private", "public", "protected". If it's only a very rare case, then please, put it somewhere away from regular "private", "public", "protected" in the handbook to prevent people's confusion.