Skip to content

Optional chaining with default parameter fails #36295

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

Closed
eventualbuddha opened this issue Jan 18, 2020 · 11 comments · Fixed by #38130
Closed

Optional chaining with default parameter fails #36295

eventualbuddha opened this issue Jan 18, 2020 · 11 comments · Fixed by #38130
Assignees
Labels
Bug A bug in TypeScript

Comments

@eventualbuddha
Copy link

TypeScript Version: 3.7.2 & Nightly (2020-01-18)

Search Terms: optional chaining, default parameters, _a is not defined

Code

const a = (): { d: unknown } | undefined => undefined;
((b = a()?.d) => {})();

Expected behavior:

This should run without error.

Actual behavior:

This throws an error: ReferenceError: _a is not defined

Playground Link: link

Related Issues: n/a

Here's the code it compiles to now for illustration:

"use strict";
const a = () => undefined;
((b = (_a = a()) === null || _a === void 0 ? void 0 : _a.d) => { var _a; })();

This error happens because _a is used before it's defined. TypeScript adds a var _a; inside the function body, but because the assignment occurs in the parameter _a is not in scope and the assignment fails. I'm not sure what the preferred solution would be here. I solved it in decaffeinate by using a helper function, though I never really liked that solution.

@ajafff
Copy link
Contributor

ajafff commented Jan 18, 2020

This looks very similar to #29159.
The problem is basically every downleveled syntax >ES6 used within a parameter initializer when targeting at least ES6 if there's the need for a temp variable.
Some examples: optional chaining, nullish coalescing, object rest, class expressions with private fields, compound assignment with exponentiation, ...

Note that this is only an error in strict mode. In sloppy mode it will run just fine, but reference the wrong variable.

@eventualbuddha
Copy link
Author

Ah, good find. I didn’t see that when I searched. It’s true that it’s only a problem in strict mode, but that’s the vast majority of code I write because I’m using ES modules. Also only a problem when targeting >ES5, but again that’s almost everything I write because certain features don’t work well otherwise, such as for-of. Not true for everyone else?

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Feb 6, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.9.0 milestone Feb 6, 2020
@lourd
Copy link

lourd commented Feb 18, 2020

Also just ran into this. Here's an alternative repro if it helps.

In discussing this with some coworkers, they brought up this note from this section of the spec:

NOTE: A separate Environment Record is needed to ensure that closures created by expressions in the formal parameter list do not have visibility of declarations in the function body.

It appears that is not being followed in the current implementation.

@evmar
Copy link
Contributor

evmar commented Feb 18, 2020

In particular, it depends on whether the code is in strict mode or not.

@rbuckton
Copy link
Member

rbuckton commented Apr 8, 2020

There are two ways to address this:

  1. Wrap the initializer in an IIFE as there's no other place to introduce the temporary variable. For example, the initializer would have to be changed to this in the emit:

    "use strict";
    const a = () => undefined;
    ((b = (() => {
      var a;
      return (_a = a()) === null || _a === void 0 ? void 0 : _a.d;
    })()) => { })();
  2. Alternatively, we need to down-level arrow function initializers using the same mechanism we use when targeting ES5:

    "use strict";
    const a = () => undefined;
    ((b) => {
      var _a;
      if (b === void 0) b = (_a = a()) === null || _a === void 0 ? void 0 : _a.d;
    })();

@rbuckton
Copy link
Member

rbuckton commented Apr 8, 2020

@DanielRosenwasser any preference?

@rbuckton
Copy link
Member

rbuckton commented Apr 8, 2020

As @ajafff pointed out, we'd need to do the same thing for #29159.

@DanielRosenwasser
Copy link
Member

I think the second as long as it's not too much work.

By the way, there's also #2584

@rbuckton
Copy link
Member

Some clarification, #29159 is actually working as intended as it is illegal for the initializer of one parameter to reference another parameter that follows it. However, the following is still incorrect:

// ts
// @target: ES6
((b = class { static x = 1 }) => {})();

// js
"use strict";
((b = (_a = class {
    },
    _a.x = 1,
    _a)) => { var _a; })();

@rbuckton
Copy link
Member

@DanielRosenwasser I have a fix that merely moves the initializer to the body, but that might mean that we need to issue an error when there are shadowed identifiers in the body that were references in the initializer:

function a() {}
function b(x = a()) {
  let a;
}

We issue an error for example above when targeting ES5 but not when targeting ES2015 or later (as the let a is not in scope when evaluating the parameter x, but with this change we will probably also need to issue an error for any edition of ES that doesn't support the syntax in the initializer:

// ts
// @target: ES2015
function a() {}
function b(x = a()?.b) {
  let a;
}

// js
function a() {}
function b(x) {
  if (x === void 0) { x = (_a = a()) === null || _a === void 0 ? void 0 : _a.d; }
  let a;
}

This will move the definition to the body, just as we do in the ES5 emit and run into the scoping issue.

@rbuckton
Copy link
Member

Reporting the correct error is not impossible but would be significantly more difficult than using an IIFE and preserving the scope:

// js
function a() {}
function b(x = (() => { var a; return (_a = a()) === null || _a === void 0 ? void 0 : _a.d; })()) {
  let a;
}

With the IIFE there's no need to move the initializer so the scope is preserved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants