Skip to content
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

The future of the "private" keyword #31670

Closed
G-Rath opened this issue May 30, 2019 · 68 comments
Closed

The future of the "private" keyword #31670

G-Rath opened this issue May 30, 2019 · 68 comments
Labels
Discussion Issues which may not have code impact

Comments

@G-Rath
Copy link

G-Rath commented May 30, 2019

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

@RyanCavanaugh RyanCavanaugh added the Discussion Issues which may not have code impact label May 30, 2019
@AnyhowStep
Copy link
Contributor

AnyhowStep commented May 30, 2019

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
Copy link
Member

RyanCavanaugh commented May 30, 2019

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

Reasons for this:

  • We don't make breaking changes for no reason, and nothing external is forcing people to move off of compile-time private
  • There are very good use cases for compile-time-only privacy:
    • private fields can be read from unit tests (I realize some people find this personally distasteful, but this is not universal)
    • private fields have much better runtime perf in downlevel scenarios
  • Lots of people don't like the # syntax so why force it on them
  • Without WeakMap (which not all runtimes have) there's no good equivalent downleveling for hard runtime privacy

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

@RyanCavanaugh
Copy link
Member

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
Copy link

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
Copy link

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
Copy link
Member

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
Copy link

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
Copy link
Contributor

mheiber commented May 31, 2019

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

@gsathya
Copy link

gsathya commented May 31, 2019

Hi I was pointed to this thread by folks regarding the runtime performance concerns mentioned in #31670 (comment). (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
Copy link

zhuravlikjb commented May 31, 2019

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
Copy link

nicojs commented May 31, 2019

  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
Copy link
Member

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
Copy link
Member

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
Copy link

fatcerberus commented May 31, 2019

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
Copy link
Contributor

ljharb commented May 31, 2019

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

@RyanCavanaugh
Copy link
Member

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

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented May 31, 2019

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
Copy link
Contributor

trusktr commented Jun 3, 2019

That shouldn't be allowed, right?

@fatcerberus
Copy link

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
Copy link

hax commented Jun 18, 2019

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
Copy link

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
Copy link

jimbuck commented Jun 20, 2019

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
Copy link
Contributor

ljharb commented Jun 20, 2019

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

@jimbuck
Copy link

jimbuck commented Jun 20, 2019

@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
Copy link
Contributor

AnyhowStep commented Jun 20, 2019

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
Copy link
Contributor

ljharb commented Jun 20, 2019

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

@fatcerberus
Copy link

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

@kokushkin
Copy link

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.

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Jan 27, 2020

My only gripe with # so far is that GitHub keeps thinking I'm trying to reference an issue, when I'm just trying to talk about private properties. It keeps trying to suggest auto complete >.>

class Foo {
  #x //GitHub unhelpfully gives me an annoying dropdown
}

Can I join the committee and reject the proposal because of GitHub inconvenience?
/joke


Okay, one more joke.

Rights,

class Human {
  #rights;

  removeRights () {
    console.log("No");
  }
}
class Government {
  takeawayRights (human) {
    human.removeRights(); //No
    human.rights = undefined; //Not allowed
  }
}

vs Privileges,

class Human {
  private privileges;

  removePrivileges () {
    console.log("No");
  }
}
class Government {
  takeawayPrivileges (human) {
    human.removePriveleges(); //No
    human.privileges = undefined; //OK!
  }
}

@hax
Copy link

hax commented Jan 30, 2020

@RyanCavanaugh

I think people are overthinking this.

I think people are too optimistic in this topic ^_^ , the interesting part is the fans of TS normally in the other side, that's why they choose TS which can provide more protection than JS. Unfortunately the great success of TS also cause many ignorance of JS, many TS programmers just say: I don't care about JS any more, I just use TS!

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?).

All these examples are just stylistic which I don't think comparable to this issue. Actually it's very dangerous that people think #foo vs private foo is just stylistic. In simple cases there is no much difference, but have subtle and significant consequences when interoperated with other code.

# has its own problems and benefits, and private has its own problems and benefits. This is a boon, not a burden.

Understanding the difference of two incompatible private mechanism is definitely a burden. Especially #priv have many strangeness which rarely possible in traditional userland JS/TS classes. And such burden do not have enough benefit in most userland code.

If you want to work hard to try to confuse yourself, sure, mix # and private in the same class.

I really hope TS documentation could add caveat for that.

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.

The problem is , classes is very different than other language features, there is inheritance. Base classes and derived classes could in different packages. In many cases, you can't control the upstream and downstream of your classes. So there is no easy way to truly avoid mixing of # and private.

If you don't want to actively sabotage your own codebase, pick one or the other,

"Pick one" is easy to say but the cost of discussion/arguments/conflict/postmortem/refactoring of "pick one" will be paid by every team, every project, again and again. As I always say, we TC39 are transferring 100 people committee cost to 10,000,000 people community cost.

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.

Actually it's very hard to understand all the consequence of #priv, programmers only be taught some buzz word like "true privacy" (sounds great!). For example the infamous proxy/membrane transparency issue, I bet there only 50% programmers could figure out proxy transparency issue in one hour, and 80% programmers would never figure out membrane at all. But they will face issues in a unfortunate day (for example, use instances with private fields and proxy-based libraries like MobX together). The worst part is you do not have any real solution of such issues except roll back your code from "true privacy" to "private" which is a not very "true" private. Who to blame? Most programmers would blame themselves. And some may blame TS: TS is not as good as I used to know... :-P

Anyway I am just the "people are overthinking this."

But again, we will see what happened.

@hax
Copy link

hax commented Jan 30, 2020

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).

@mbrowne Unfortunately, when something is broadly used in production, it will be too late to remove it even it is heavily flawed. So it's meaningless to make "decision" that time.

Actually we are now in the way to such situation.

And there is a logic problem that "feedback from people who have actually used private fields in production". We the frontend branch of tech committee of 360 tech group (consist of 10+ most experienced programmer, team leaders and architects of our group, most of them have 10+ years JS experience and at least half of them are heavy TS developers) already discussed the feature thoroughly, we have the conclusion that class fields have serious problems and we will never use it in our production code. Class fields is not a small feature, and several issues of them are interoperable issues with other code in complex context which will be very hard to track, we have no interest to take any risk to test it in our production code. But as your criteria, our opinion is not valid feedback because we didn't use it in production. The problem is if we already anticipate the worst result, why we want to use it in production?

@mbrowne
Copy link

mbrowne commented Jan 30, 2020

@hax We could have a long discussion about process issues, but since this is the TypeScript repo, I'll just say that I do think the feedback of those who have investigated the feature but not used it in production is valuable (especially if they have investigated it deeply as you have done), but should definitely be considered together with feedback from usage in production. And we certainly saw some flaws in the current TC39 staging process with this feature that should be addressed in order to collect more real-world feedback (e.g. via Babel) before new features ship in browsers.

But I would ask TS users to keep JS users in mind here...JS users have wanted private state in their classes for a long time. The only other syntax I have seen that would allow for hard privacy at run-time (which I don't see as a requirement—I would be fine with soft run-time privacy—but many people do want it, including some TS users) is var/let/const syntax as in the classes 1.1 proposal and rdking's fork of that proposal. And personally, I think that TC39 is correct that that syntax is actually more confusing and problematic than the class fields (#priv) proposal. I wish more user testing had been done to verify this. But I think TypeScript's policy of implementing stage 3 ES proposals is a good policy—no one is forcing all users to immediately switch to the new syntax.

@franktopel
Copy link

franktopel commented May 8, 2020

@hax

Then some thought about private and #:
[...]
There would at least three options:

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

You forgot the very probable option

  1. use neither until the smoke clears, if ever

@spikyjt
Copy link

spikyjt commented May 10, 2020

Like many here, I dislike the # syntax for private fields, but I dislike even more the (not very obvious) "compile-time only" nature of private in TS. It's one of many TS gotchas that has caught me out. In particular when working with JS libraries where you might expect private to behave in a logical manner, e.g. with Vue components which wraps all fields in accessors in order to manage state.

Since # fields are compiled to code that produces the same behaviour as a private field, when targeting es2019 and below (using __classPrivateFieldSet and WeakMap), surely the same could be done for fields marked private? Even if maintaining BC is a requirement, this could be optional with a suitable flag in tsconfig, for those of us who always expected private to actually be private and would prefer our future code to behave appropriately.

@bcis-kassel
Copy link

I don't get the point why it would be a breaking change to have a tsconfig flag "compilePrivateToHashtag" which is disabled by default. With the flag turned (by default) off the behaviour remains the same; no breaking change. But If you like to have hard privacy enabled by using the pirvate key word instead of the # syntax your are free to set the flag.

This solution would fit all needs:
People that want soft and hard privacy => flag off, using private and #
People that want soft privacy only => flag off, using private only
People that want hard privacy loving # syntax => flag on or off, using # only
People that want hard privacy loving private keyword => flag on, using private only

With optionally tslint enabled to prefer one syntax over the other.

@ljharb
Copy link
Contributor

ljharb commented Jul 20, 2021

Why should anybody be given the ability to write new code that is nonstandard? The language has no private keyword and won’t have one; new code shouldn’t have one either.

@rjgotten
Copy link

Why should anybody be given the ability to write new code that is nonstandard? The language has no private keyword and won’t have one; new code shouldn’t have one either.

JavaScript has no access modifier keywords. But it doesn't have - e.g. type declarations either. That's why TypeScript is a superset.

There's no reason TypeScript shouldn't be allowed to map its private access modifier keyword onto #-methods moving forward; that's the discussion at hand here, afaict.

@nicojs
Copy link

nicojs commented Jul 20, 2021

It is a fair criticism to say: "you should use #foo rather than private foo", but it would be pretty opinionated and I feel like it's more in the realm of eslint plugins. There should be an eslint rule that forces you to use es privates, i.e. "prefer-es-private".

@ljharb
Copy link
Contributor

ljharb commented Jul 20, 2021

There’s no value in using TS’s private keyword semantics when the #, alone, conveys “private”, in a standard way.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 20, 2021

We're not going to add a flag for this. # means runtime private, private means compile-time private. There's nothing gained but confusion by adding a flag to conflate the two. It'd be like adding a flag to make ^ to compile to ** -- the syntaxes are unambiguous in their meaning, and preference over which you would have liked to written for any particular operation is not relevant.

I know some people have aesthetic aversion to #, but I will use my "Ryan only gets to say this once a year" token here: get over it. JS syntax is JS syntax and TS isn't here to add literal pure sugar at the expense of comprehensibility.

@mbrowne
Copy link

mbrowne commented Jul 20, 2021

It could still be nice to have an option to make private compile to some sort of soft run-time privacy though. (So not #, but maybe symbols...)

@xhermosilla
Copy link

xhermosilla commented Sep 6, 2021

Is it planned in the future to transpile (tsc) directly to #variable / #method instead of using a WeakMap ?
If the ecmascript proposal goes ahead, it wouldn't make much sense to keep a confusing and verbose transpiled code for that right?

@owenallenaz
Copy link

If we're compiling for a es2021 target, shouldn't a true #foo compile to #foo? Right now it compiles to a WeakMap which I understand for compiling to older standards, but for the latest it doesn't seem like it should do that.

@mbrowne
Copy link

mbrowne commented Jan 4, 2022

The handbook states:

When compiling to ES2021 or less, TypeScript will use WeakMaps in place of #.

I'm not sure why private fields didn't make it into the ES2021 target, but in any case it seems that the only way to get TS to output native private fields at the moment is to use esnext as the target:

"target": "esnext"

@ljharb
Copy link
Contributor

ljharb commented Jan 4, 2022

Because class fields are in ES2022: https://github.com/tc39/proposals/blob/main/finished-proposals.md

@trusktr
Copy link
Contributor

trusktr commented Dec 17, 2023

It could still be nice to have an option to make private compile to some sort of soft run-time privacy though. (So not #, but maybe symbols...)

I believe that's out of scope of TypeScript's goals (essentially the goal is to avoid producing new runtime language features, and make only strippable type syntax on top of standard JavaScript), but compiling private to soft privacy using symbols, under an option, would in fact be pretty sweet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Issues which may not have code impact
Projects
None yet
Development

No branches or pull requests