Skip to content

Overcoming this-hesitancy in TC39 #31

Open

Description

@ewinslow

A significant objection to this proposal is that this is confusing:

I'm going to go out on a limb and say probably 90% of working JavaScript developers don't understand how this works in method calls at all. And don't care. Don't have to care. Teaching somebody, "well, in this piece of code you have to use double colon and in this piece of code you have to use a period"? Not going to happen. I think that's a social problem and seems to me the technical problem of building a smarter bundler, no matter how hard that is, it's still easier than solving a giant social problem that affects the whole industry.

I figure we can discuss here the potential mitigations of this concern. Below I list some hypotheses that we could pursue further if they sound promising. Any of these hypotheses will likely need to be strongly justified by data to be persuasive.

  1. Most code and jobs use TypeScript. TypeScript provides built in protection against accidental misuse of this. Its error messages can provide just-in-time education that will reach a huge portion of the community relatively quickly and cheaply. EDIT: There are still cases where protection is lost today and perhaps it is worthwhile to tighten up the protection there. I personally have found the existing protections for this-using standalone functions to be quite helpful.

  2. Presuming that the goal is to make classes tree-shakable for the sake of end users, we can get most of the end user benefit by convincing a relatively few developers to switch to new syntax. So this may not actually be a "whole industry" kind of problem. In particular, I have in mind the maintainers of:

    1. Broadly-used platforms like Wix, SquareSpace, Wordpress, etc
    2. High traffic sites like those listed at https://www.semrush.com/website/top/
    3. High-usage libraries by npm downloads and/or GitHub stars
  3. The new syntax will make learning this easier (as long as the educator's goal is to provide understanding and not just find excuses to poke fun at JavaScript). Since it provides a syntax that is as convenient as passing arguments, we can treat it as the canonical/verbose form and others as shorthand:

    • o..fn() calls fn with this === o
    • undefined..fn() calls fn with this === undefined. In strict mode, you can write this as fn() for short
    • o..(o.fn)() calls o.fn with this === o. You can write this as o.fn() for short.
    • Caveats for arrow functions, bound functions, and non strict mode.
  4. There is no way to teach bundlers to delete class methods completely safely. It's literally not possible, given the dynamic nature of JS. The only option without new syntax is to give them permission to take more risk. But this is also a social problem, not a technical one. Convincing people to take more risk, write more tests, enable strict lints, avoid packages that trigger deoptimization, and learn how to debug the failures when prod behavior deviates from dev, is it's own set of social problems.

  5. Smarter bundlers are necessarily slower. They must do whole program analysis, just to remove blocks of code that ideally would never have made it into the bundle in the first place. This wastes developer time on an ongoing basis by extending build and CI times.

  6. More aggressive bundlers inherently implies increasing the distance between dev and prod semantics, which makes final output harder to reason about. Developers with have to learn the idiosyncrasies of individual bundlers, which is not necessarily transferable knowledge vs learning the behavior of this, which is highly transferable.

  7. Companies and libraries whose users could benefit substantially from tree shaking will also invest in educating their devs (and per point 3, I don't think this will be nearly as complicated as people fear).

  8. In general, it's better to transfer cost from end users to developers because smaller more concentrated groups are in a much better position to minimize costs, rather than keeping costs diffuse.

  9. The way call-this is defined currently, it is possible to offer class-based API without additional wrapper functions for those who do not care about the tree-shaking tradeoff and/or just need backwards compatibility during migration.

  10. We can do both (improve the language and improve the bundlers). We want "defense in depth" against dead code. Smarter bundlers and DCE-friendly language features complement each other, rather than compete with each other. Dead code is a big problem so it's better to attack it from multiple angles.

Activity

  1. theScottyJam commented on Mar 16, 2026

    @theScottyJam
    Contributor

    Not trying to derail your general idea, but gonna pick on this point a bit.

    Most code and jobs use typescript. Typescript provides built in protection against accidental misuse of this.

    Unfortunetally, it usually doesn't.

    class MyThing {
      x = 2;
      method() {
        return this.x;
      }
    }
    
    const thing = new MyThing();
    setTimeout(thing.method, 100); // Just fine
    thing.method.call({}); // No problem
    

    I don't know if this is because of a technical limitation, backwards compatibility restraints, they just haven't gotten arround to it, or what. But this in TypeScript isn't all that type-safe, and I wish it were.

  2. ewinslow commented on Mar 16, 2026

    @ewinslow
    Author

    Not derailing at all, this is exactly the kind of discussion that needs to happen. I have no interest in making decisions based on bad arguments or data, so thank you. Will take a look later and figure out the best way to clarify.

  3. ljharb commented on Mar 16, 2026

    @ljharb
    Member

    While the many vagaries of this are indeed not widely known, I think the vast majority of JS devs understand the basic idea that in a.b(), a is the this value inside the a.b method.

  4. robbiespeed commented on Mar 16, 2026

    @robbiespeed

    This focuses a lot on the advantages the feature brings bundlers. I don't find those arguments as compelling compared to what is gained from the usage and authoring perspective.

    As an author this gives me better freedom to separate core class methods from methods that may be more experimental or supplementary. It also opens up opportunity for third parties to author their own supplementary methods.

    One can argue that ability roughly exists already by choosing functions with first argument as the recieved rather than this, but that forces different calling conventions. In turn it makes the decision of what to embed as core methods harder.

    As a user I get consistent receiver first calling convention for a much larger range of methods. Additionally I highly suspect once language service support of this feature matures I would benefit from improved suggestions/auto complete, since suggestions could be filtered via receiver type.

  5. ewinslow commented on Mar 16, 2026

    @ewinslow
    Author

    @robbiespeed IIUC you're saying you strongly disagree with this particular section of the feedback:

    Teaching somebody, "well, in this piece of code you have to use double colon and in this piece of code you have to use a period"? Not going to happen.

    Not only do you think it can happen, but you find it to be much more attractive than the alternatives that people currently have to resort to. If we can have methods that are detached from classes, but still callable on instances in a chainable way, we can experiment more freely with APIs without giving up the familiar calling convention of receiver-method-args, which people are quite comfortable with, contrary to the quoted concern.

    I share this intuition. I'd be curious if you (or anyone) can think of a way we might validate or invalidate this intuition with data.

    One can argue that ability roughly exists already by choosing functions with first argument as the recieved rather than this, but that forces different calling conventions

    This committee member is complaining that methods written for call-this also result in a different (and even brand new) call syntax, and that during any incremental transition, there will be confusion I.e. sometimes you have to do o.m(), other times you have to do o..m() instead. I think I'd say the difference between o.m() and o..m() is quite minor, means exactly the same thing as the other when o.m === m, autocomplete can help a ton, other mistakes and typos can be easily caught by TS and are cheaply corrected, and it retains the familiar features of receiver-method-args ordering, autocomplete, and chaining.

    Any other ideas on how you'd persuade someone worried about developer confusion over this?

  6. ewinslow commented on Mar 17, 2026

    @ewinslow
    Author

    RE #31 (comment):

    I tried more some more cases, and got basically what I expected. Actually it appears standalone methods have more protection than class methods because... I don't know. Not clear that's intentional on TS's part...

    // deps.ts
    export class MyThing {
      x = 1;
    }
    
    export function method(this: MyThing) {
      return this.x;
    }
    
    // main.ts
    import * as deps from './deps';
    
    // Correct error
    deps.method.call({});
                     ^^ Argument of type '{}' is not assignable to parameter of type 'MyThing'.
    
    // Correct error
    deps.method(); 
    ^^^^ The 'this' context of type 'typeof import("file:///deps")' is not assignable to method's 'this' of type 'MyThing'.
    
    // Should error, but doesn't :(
    setTimeout(deps.method, 100);
    
    // OK!
    deps.method.call(new deps.MyThing());

    If I define a this-using function without declaring its this type at all, I get complaints:

    export function method() {
      return this.x;
             ^^^^ 'this' implicitly has type 'any' because it does not have a type annotation.
    }

    These are the sorts of protections I had in mind. The setTimeout issue is indeed unfortunate. Perhaps it can be improved...

  7. robbiespeed commented on Mar 17, 2026

    @robbiespeed

    IIUC you're saying you strongly disagree with this particular section of the feedback

    Teaching somebody, "well, in this piece of code you have to use double colon and in this piece of code you have to use a period"? Not going to happen.

    Yes. Any language change will require some amount of learning. Symbols, private fields, optional chaining, are all examples of language features which have successfully changed the way to access properties or call methods.

    during any incremental transition, there will be confusion I.e. sometimes you have to do o.m(), other times you have to do o..m() instead

    No one is forcing incremental transition, that'll be a decision for libraries and users. The difference will be fairly clear since in the call-this scenario the function will need to be imported or in scope, just like a symbol.

    To that point though I do have concerns about the syntax looking too similar, and would rather we go with a syntax that more clearly distinguishes itself like o::m().

    Any other ideas on how you'd persuade someone worried about developer confusion over this?

    Other than what's already stated, I would try to remind any worried party that developers are quite adaptable (I dare say it's a required trait). And in many cases developers are eager to learn new syntax features.

  8. ewinslow commented on Mar 18, 2026

    @ewinslow
    Author

    Added a 10th point to emphasize that "make bundlers smarter" and "make JS tree-shakeable" are not mutually exclusive strategies. Surely TC39 can improve the language and bundler folk can improve the bundlers for "defense in depth" against dead code.

  9. hax commented on Mar 26, 2026

    @hax
    Member

    I agree with most of the opinions here. In my experience, the confusion around this largely stems from some clickbait articles or books that exaggerate the issues with this, creating a stereotype within the developer community. However, we must also acknowledge that any enhancement of features around this—even with good intentions and excellent design—inevitably expands the body of knowledge about this and increases confusion, at least for a period of time (arrow functions are an example). So I think one way to measure this is how the call-this proposal can avoid such problems as much as possible. This might sound somewhat subjective, but we can now use a more objective approach—by observing whether AI becomes confused.

    Interestingly, I've found that fn(this: rec, ...args) syntax is very close to the ideal state. Conversely, o..method is more prone to misinterpretation (as it looks more like a typo to AIs). Of course, the specifics of syntax might be better discussed in #10.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions