Sitelet https://github.com/microsoft/TypeScript/issues/31670#issuecomment-497370557
Skip to content

The future of the "private" keyword #31670

Description

@G-Rath

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

Activity

  1. AnyhowStep commented on May 30, 2019

    @AnyhowStep
    Contributor

    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.

  2. RyanCavanaugh commented on May 30, 2019

    @RyanCavanaugh
    Member

    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 legitimate use cases for compile-only privacy, e.g. private fields can be read from unit tests (I realize some people find this personally distasteful, but this is not universal)
    • 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.

  3. RyanCavanaugh commented on May 30, 2019

    @RyanCavanaugh
    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.

  4. fatcerberus commented on May 30, 2019

    @fatcerberus

    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.

  5. fatcerberus commented on May 30, 2019

    @fatcerberus

    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.

  6. RyanCavanaugh commented on May 30, 2019

    @RyanCavanaugh
    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

  7. fatcerberus commented on May 30, 2019

    @fatcerberus

    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.

  8. mheiber commented on May 31, 2019

    @mheiber
    Contributor

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

  9. gsathya commented on May 31, 2019

    @gsathya

    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?

  10. zhuravlikjb commented on May 31, 2019

    @zhuravlikjb

    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.
  11. nicojs commented on May 31, 2019

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

  12. RyanCavanaugh commented on May 31, 2019

    @RyanCavanaugh
    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.

  13. RyanCavanaugh commented on May 31, 2019

    @RyanCavanaugh
    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

  14. fatcerberus commented on May 31, 2019

    @fatcerberus

    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?

  15. 53 remaining items

  16. trusktr commented on Dec 17, 2023

    @trusktr
    Contributor

    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.

  17. ArrayIterator commented on Oct 23, 2025

    @ArrayIterator

    2025! the private visibility modifier can be dangerous on some case, eg : doing Stringify object, the private modifier can be exposed on JSON.

  18. Finesse commented on Mar 19, 2026

    @Finesse

    There's nothing gained but confusion by adding a flag to conflate the two.

    There is something gained. The #-fields can be shortened easily, similar to how function, class and variable names are minified. Tools like Terser replace long #-names with short garbage. This gives 2 advantages:

    • Better concealing of the source code.
    • Better code size deduction from minification.

    Do you have other options for achieving these goals? Replacing all private fields with #-fields in the source code is a solution, but it's awkward for multiple reasons. Having tsc do a prepending of # to the private fields is such a simple and elegant solution, that I'm surprised it hasn't been implemented yet. I call it simple, because it can be done in a file using just the content of that file. That is, it doesn't violate isolatedModules.

    because that would require type-directed emit

    Why is it allowed for const enum? It's also an elegant solution for source code concealing and code size deduction. What's the key difference?

  19. snarbles2 commented on Mar 19, 2026

    @snarbles2

    Having tsc do a prepending of # to the private fields is such a simple and elegant solution, that I'm surprised it hasn't been implemented yet.

    It really, really isn't that simple. private and # have different semantics. Transpiling the former to the latter can break things in all kinds of ways.

    You would have to change the meaning of private, but the (subjective) upsides of having a "soft-private" and backwards compatibility concerns make for a rather strong argument against doing so, to put it mildly.

    You could argue for having a compiler switch, but if people find it confusing that # and private mean similar but not identical things, you're not doing the world favors by adding into the mix that private could mean two different things depending on compiler version or settings.

    Why is it allowed for const enum? It's also an elegant solution for source code concealing and code size deduction. What's the key difference?

    It is grandfathered in, a vestige from before TypeScript adopted the position against type-directed emit.

  20. irfanstract commented on Aug 14, 2026

    @irfanstract

    the private visibility modifier can be dangerous on some case

    so we should start warning against private and call for rewrite.

         private lastChgd: number;
    +    ^^^^^^^
    +    classic 'private'. consider migrating to ES Private Fields.
    abstract class LivingDocument {
      private lastChgd: number;
      ...
    }

    Why is it allowed for const enum?

    certain transpiler(s) drop const and treat them like plain enum maybe?

  21. RyanCavanaugh commented on Aug 17, 2026

    @RyanCavanaugh
    Member

    You can add a lint rule if you don't want to use private. We're not going to break huge amounts of code for a "warning" that doesn't do anything.

  22. ljharb commented on Aug 17, 2026

    @ljharb
    Contributor

    What about a tsconfig option that removes it entirely?

  23. snarbles2 commented on Aug 17, 2026

    @snarbles2

    Why would you want to remove it? It has well understood semantics that can be useful. I'm not sure any of the considerations that factored into to the current state of affairs have changed meaningfully.

  24. RyanCavanaugh commented on Aug 17, 2026

    @RyanCavanaugh
    Member

    Why have a flag for banning private vs any other syntactic feature you might not like? I don't get the distinction

  25. rjgotten commented on Aug 17, 2026

    @rjgotten

    Why have a flag for banning private vs any other syntactic feature you might not like? I don't get the distinction

    Why have a flag for banning any then?
    private arguably does more real-world harm than any - given private isn't actually private and the corpus of code written by neophytes with wrong expectations about it.

  26. ljharb commented on Aug 17, 2026

    @ljharb
    Contributor

    Ryan Cavanaugh (@RyanCavanaugh) there's a large qualitative difference between banning a type-space syntax and banning a value-space syntax, especially one that will never be standard, and whose emit provides false encapsulation.

    snarbles2 none of its semantics are useful - if you want truly private data, use JS private fields; if you don't, just use symbols or strings (like, a leading underscore, or whatever). private is no different than the latter.

  27. snarbles2 commented on Aug 18, 2026

    @snarbles2

    Certainly not true. There are things you can do with private that you can't with #private, and private is still compiler enforced. This even comes up in vanilla JS, where you have to resort to _prefixes.

    You can use symbols for that purpose, I suppose, but I have a hard time buying that I should sacrifice ergonomics/readability to satisfy somebody else's notion of what's "right".

    I'm usually the last person to make this suggestion, but a linter is the right solution here.

  28. mbrowne commented on Aug 18, 2026

    @mbrowne

    One thing that might help would be to say that the private keyword is deprecated in the official docs. Obviously it won't ever be removed because of the need to preserve backward compatibility, but it's non-standard and IMO it would make sense to officially deprecate it given native private and other options available.

  29. rjgotten commented on Aug 18, 2026

    @rjgotten

    There are things you can do with private that you can't with #private

    And you can only do those things because private is misnamed and actually at runtime is anything-but.

    I'm usually the last person to make this suggestion, but a linter is the right solution here.

    An option in the compiler to disallow private - pretty much the same as the one that disallows any - for all intents and purposes is a linter option.

    You can use symbols for that purpose, I suppose, but I have a hard time buying that I should sacrifice ergonomics/readability to satisfy somebody else's notion of what's "right".

    I would be all for TypeScript inventing a syntax for symbol usage that could desugar properly without runtime surface as well as make unique symbol easier to stomach within the typing system. Because woo-boy is that a pain in the dot-dot-dot to work with...

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

    DiscussionIssues which may not have code impact

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions