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

Private named instance fields #30829

Merged

Conversation

mheiber
Copy link
Contributor

@mheiber mheiber commented Apr 9, 2019

Private-Named Instance Fields

This PR implements the tc39 class fields proposal for TypeScript. It includes:

  • parse and check private-named fields, methods, and accessors
  • displayprivate names in the language server
  • transform private-named instance fields

PR merge checklist

  • BB: incorporate remaining feedback
  • BB: add multiple @targets to conformance tests esp this one
  • MS: reviewer 1 👍 remaining feedback
  • MS: reviewer 2 👍 remaining feedback
  • BB: squash and rebase
  • MS: run rw test suite
  • merge companion PR to tslib

Example:

ts

class Greeter {
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }
    greet() {
        console.log(`hello ${this.#name}`);
    }
}

const greeter = new Greeter("world");
console.log(greeter);                 // logs 'Greeter {}'
console.log(Object.keys(greeter));    // logs '[]'
greeter.greet();                      // logs 'hello world'

js

var _classPrivateFieldSet = function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to set private field on non-instance"); } privateMap.set(receiver, value); return value; };
var _classPrivateFieldGet = function (receiver, privateMap) { if (!privateMap.has(receiver)) { throw new TypeError("attempted to get private field on non-instance"); } return privateMap.get(receiver); };
var _nameWeakMap_1;
class Greeter {
    constructor(name) {
        _nameWeakMap_1.set(this, void 0);
        _classPrivateFieldSet(this, _nameWeakMap_1, name);
    }
    greet() {
        console.log(`hello ${_classPrivateFieldGet(this, _nameWeakMap_1)}`);
    }
}
_nameWeakMap_1 = new WeakMap();
const greeter = new Greeter("world");
console.log(greeter); // logs 'Greeter {}'
console.log(Object.keys(greeter)); // logs '[]'
greeter.greet(); // logs 'hello world'

This PR lead to the following related work by the team:

This PR includes work by the following engineers:

@msftclas
Copy link

msftclas commented Apr 9, 2019

CLA assistant check
All CLA requirements met.

@mheiber mheiber force-pushed the private-named-instance-fields branch 2 times, most recently from b242db5 to f2c5233 Compare April 9, 2019 12:33
@mihailik
Copy link
Contributor

Amazing achievement!

Are the helpers _classPrivateFieldGet/_classPrivateFieldSet treated in the same way as __extends, __awaiter, __generator?

(that would include modifications to tslib)

@mheiber
Copy link
Contributor Author

mheiber commented Apr 17, 2019

@mihailik thanks! As far as I know, the helpers work the same way. Is any additional work required to get the helpers into tslib? I was hoping the syncing was semi-automated.

@weswigham
Copy link
Member

I was hoping the syncing was semi-automated.

Sadly, they are not. A paired PR against tslib with the finalized helpers will be needed.

@Neuroboy23
Copy link

@weswigham: I have asked our IP team to set up a fork of the tslib repo.

@joeywatts and @mheiber: Will you take a look at this other repo? I think it will be an easy mod.

@G-Rath
Copy link

G-Rath commented May 30, 2019

Forgive me if this is obvious, but I've look around on the issues and couldn't see this covered anywhere:

Is there any particular reason why we can't do private #bar = 3;?
(In case its not clear, I mean this purely as a meaningless sugar that would never make it into compiled code)

Would there be any chance of getting that syntax supported?

In my eyes it'd give us the best of both worlds, as then all class properties can maintain their indentation level, along with making refactoring easy for people like me who prefix all our private properties with _.

I mean you could pretty much just use a global find-and-replace using /private #/ regex, since # is only valid for private class properties - the only issue with this I can think of would be with strings.

Also, could someone confirm for me if its planned for TS somewhere down the line to transform private to #? (I suspect it is, but again there's a lot of heavy comments, PRs, & issues on the matter, so it'd be nice to have a simple one-liner comment about it if possible).

@ljharb
Copy link
Contributor

ljharb commented May 30, 2019

@G-Rath no, no chance of it being supported by JS itself (see the FAQ). I suspect/hope TS would not be likely to support non-type-related syntax that isn't standard to JS.

@G-Rath
Copy link

G-Rath commented May 30, 2019

@ljharb fair enough - so then I assume TS is aiming to deprecate & remove the private keyword? (over time of course)

@robpalme
Copy link

@G-Rath Your question on the future of TypeScript's keyword private is one that many people will have, and may provoke more discussion. Would you like to raise it as a new top-level issue?

@G-Rath
Copy link

G-Rath commented May 30, 2019

@robpalme more than happy to - done via #31670

@septs
Copy link
Contributor

septs commented May 31, 2019

"private field" decorator "private" only?

class Sample {
    private #field: number = 1;
}
@robpalme
Copy link

@rbuckton has re-landed the refactoring of class properties. The next step is to rebase this PR.

@Aqours
Copy link

Aqours commented Jun 21, 2019

Does private name use same private fields transformation of #name?

@DanielRosenwasser
Copy link
Member

@Aqours no, private is meant to be strictly design-time.

@mheiber
Copy link
Contributor Author

mheiber commented Jun 21, 2019

The rebase is in progress and we're expecting to update soon!

@mheiber
Copy link
Contributor Author

mheiber commented Jun 21, 2019

I noticed an issue with our Language Service changes. Type inference and red underlines work correctly, but autocomplete is borked.

Steps to reproduce:

In a class with private field #foo#, start typing this.#foo

Actual behavior:

langservice

Autocomplete completes incorrectly (see screenshot). It is actually completing with secret variables we use in the transformation

Expected behavior:

Autocomplete completes as #foo.

Advice welcome on how to fix autocomplete in this PR!

@DanielRosenwasser
Copy link
Member

That is actually really weird. Are you sure that it's not just picking up the output .js files? Are you able to reproduce in a fourslash test?

@mheiber mheiber force-pushed the private-named-instance-fields branch from f2c5233 to a9cb10e Compare June 25, 2019 08:47
@mheiber
Copy link
Contributor Author

mheiber commented Jun 25, 2019

Thanks for your suggestion, @DanielRosenwasser: the completions were picking up the JS output. I added an export to let TS know the file is a module, and no longer see weird completions.
Unfortunately, I also don't see the desired completion, which is this.#foo:

image

@joeywatts joeywatts force-pushed the private-named-instance-fields branch from a9cb10e to 36a1648 Compare June 26, 2019 19:31
@mheiber mheiber force-pushed the private-named-instance-fields branch from 36a1648 to d2adc3c Compare June 27, 2019 10:31
@joeywatts joeywatts force-pushed the private-named-instance-fields branch 2 times, most recently from 71c384a to afcc88c Compare June 27, 2019 21:23
@littledan
Copy link

Private fields are deliberately designed to be not hackable: strong encapsulation is a core design goal. I'd recommend strongly against manipulating WeakMap's prototype for this purpose--it won't hold up when you later upgrade to native private fields (which have better performance).

If you want hackable privacy, you can continue to use the TypeScript private keyword.

@mheiber
Copy link
Contributor Author

mheiber commented Dec 31, 2019

fwiw, I agree that the hack is a bad idea. Wouldn't want the hack to be a secret, though, since it's good to know that the privacy of the transformed code is not absolute, barring control over the WeakMap prototype. @littledan, are there other privacy hills privacy holes, as well?

@littledan
Copy link

@mheiber What do you mean by privacy hills?

@mheiber
Copy link
Contributor Author

mheiber commented Dec 31, 2019

I meant "privacy holes." Autocorrect got me!

@littledan
Copy link

littledan commented Dec 31, 2019

I'm not aware of any privacy holes in the main design. There may be other hacks that just work on this transform, though.

@Jamesernator
Copy link

Jamesernator commented Jan 2, 2020

We have no plans to implement #-private parameter properties.

The feature is pretty popular, I imagine a lot of places won't adopt private fields primarily for this reason.

Has a similar feature been considered for proposing at TC39? e.g.:

class Point {
    constructor(#x: number, #y: number) {}
}

And for public fields maybe allow using this.prop as a parameter (or destructured parameter) e.g.:

class Image {
  constructor(
    this.data: ImageData, // Simple parameter
    {
      height: this.height,
      width: this.width /* in destructuring */
    }: { width: number, height: number },
  ) {}
}
@avonwyss
Copy link

avonwyss commented Jan 2, 2020

Given that the __classPrivateFieldGet and __classPrivateFieldSet could be customized in the way they work by implementing them differently and importing that customized tslib, wouldn't it make sense to also put the _nameWeakMap_1 = new WeakMap(); map initialization into its own tslib helper function?

This would allow to implement a version which also works without WeakMap support (e.g. IE <= 10), or for anyone who wants to use a different mechanism, such as with non-enumerable properties or Symbol-based private fields (knowing that these are not private as per tc39 proposal, but may perform better).

@mbrowne
Copy link

mbrowne commented Jan 3, 2020

In case anyone else is wondering the same thing I was, it looks like the first production release to include this will be version 3.8, scheduled for sometime in February:
https://github.com/microsoft/TypeScript/wiki/Roadmap#38-february-2020

sheetalkamat added a commit to microsoft/TypeScript-TmLanguage that referenced this pull request Jan 8, 2020
@kokushkin
Copy link

This looks ridiculous and disrupt developer experience with the language, especially for newers) Please, don't do it. This is feature for Typescript 4, not for 3.8, where we can break backward compatibility. Or, at least either "private" or "#" must be deleted in Typescript 4.

@mbrowne
Copy link

mbrowne commented Jan 24, 2020

@kokushkin Obviously # should not be dropped in TypeScript 4 since it's already an ECMAScript standard (still stage 3, but advancing to stage 4 is just a formality at this point in the process). And I think dropping private would be premature since it would break backward compatibility and there is currently no other way to create compile-time only private properties (or at least properties that are accessible somehow outside the class, e.g. symbols). Anyway, this PR isn't the best place for these discussions...just thought I would briefly give my two cents in response to this.

@hax
Copy link

hax commented Jan 27, 2020

since it's already an ECMAScript standard

This is not true. See my previous comment.

@mbrowne
Copy link

mbrowne commented Jan 27, 2020

@hax You're right, I didn't mean to mislead anyone, just should have written more carefully... I was trying to emphasize that this isn't just an early-stage proposal that TypeScript decided to implement on their own. I should have said it's a proposed standard that's very near the final stage of the standardization process, with private fields now enabled by default in Chrome, Firefox, and node.

@jkrems
Copy link

jkrems commented Jan 27, 2020

with private fields now enabled by default in Chrome, Firefox, and node.

Maybe you meant s/Firefix/Edge/? Firefox has public instance fields but no real support for private fields yet. Though it looks like it's just a matter of time until they get to it: https://bugzilla.mozilla.org/show_bug.cgi?id=1562054. But >50% of web users have private fields enabled so your overall point stands I think. :)

@mbrowne
Copy link

mbrowne commented Jan 27, 2020

@jkrems Ah yes, thanks for the correction. The public fields part of the proposal is enabled by default in Firefox...next step will probably be to add private fields behind a flag. Implementation status is periodically updated here:
https://github.com/tc39/proposal-class-fields#implementations

@danielearwicker
Copy link

A question about the downlevel emit - to use this feature I have to target ES2015. But that will mean classes remain as classes, which breaks IE11. And yet IE11 supports WeakMap to some extent. And I find that TS does actually produce what looks like a good emit private fields for ES5, but gives me an error message.

Is IE11's WeakMap good enough for this usage? It returns undefined from set but the TS emit here doesn't care about that.

If I could suppress the error somehow... #29950

kdesysadmin pushed a commit to KDE/syntax-highlighting that referenced this pull request Feb 29, 2020
…e fixes

Summary:
* Add private-named instance fields. Ex: `x.#name;`. [1]
* Support type-only imports and exports. [2]
* Improve the detection of conditional expressions `a ? b : c`. Allow multiple lines.
* Add rules of round brackets `()` to correct the highlighting of pairs of brackets. [3]

[1] microsoft/TypeScript#30829
[2] microsoft/TypeScript#35200
[3] https://unix.stackexchange.com/questions/527268/kate-18-12-3-no-longer-shows-matching-parenthesis-for-typescript

Reviewers: #framework_syntax_highlighting, dhaumann, cullmann

Reviewed By: #framework_syntax_highlighting, cullmann

Subscribers: kwrite-devel, kde-frameworks-devel

Tags: #kate, #frameworks

Differential Revision: https://phabricator.kde.org/D27692
@trusktr
Copy link
Contributor

trusktr commented May 14, 2020

The implementation could use a single WeakMap per module, instead of one WeakMap per property. It think it may be more efficient.

Maybe I missed it: is there a specific reason it needs to be one WM per property?

@ljharb
Copy link
Contributor

ljharb commented May 14, 2020

At the least you’d need one for statics, and one for instances - but actually, it wouldn’t necessarily be more efficient, since you’d need a containing object to look up the individual fields in - and for functions, you’d have to be very careful not to expose the containing object as the receiver.

@trusktr
Copy link
Contributor

trusktr commented May 14, 2020

Ah, right! I take that back, because for each instance, we'd need that accompanying object, which means O(n) instead of O(1) mem use for storage containers for the key-value pairs, where n is number of class instances and 1 is the constant number of defined private fields. I overlooked that.

@mbrowne
Copy link

mbrowne commented May 22, 2020

@trusktr FYI, there's also this issue: https://github.com/tc39/proposal-class-fields/blob/master/PRIVATE_SYNTAX_FAQ.md#how-can-you-model-encapsulation-using-weakmaps.

Babel works around this by creating a separate WeakMap for each field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment