esbuild icon indicating copy to clipboard operation
esbuild copied to clipboard

Experimental class decorator will degrade private fields.

Open gwsbhqt opened this issue 4 months ago • 2 comments

experimentalDecorators false:

const decorate = (() => {}) as any;

@decorate
export class Class {
  #field = "field";
}

/**
 * output

const decorate = () => {
};
@decorate
export class Class {
  #field = "field";
}

 */

experimentalDecorators true:

const decorate = (() => {}) as any;

@decorate
export class Class {
  #field = "field";
}

/**
 * output

var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
  for (var i = decorators.length - 1, decorator; i >= 0; i--)
    if (decorator = decorators[i])
      result = (kind ? decorator(target, key, result) : decorator(result)) || result;
  if (kind && result)
    __defProp(target, key, result);
  return result;
};
var __privateAdd = (obj, member, value) => {
  if (member.has(obj))
    throw TypeError("Cannot add the same private member more than once");
  member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
};
var _field;
const decorate = () => {
};
export let Class = class {
  constructor() {
    __privateAdd(this, _field, "field");
  }
};
_field = new WeakMap();
Class = __decorateClass([
  decorate
], Class);

 */

How can I keep a private field under an experimental decorator?

gwsbhqt avatar Feb 20 '24 12:02 gwsbhqt

That was because experimentalDecorators implies lowering all static fields to avoid the issue when referencing the class name in static fields, which implies lowering all instance fields to avoid the issue when a static field gets private instance field outside of the class.

https://github.com/evanw/esbuild/blob/40711afe0baa545012b813c5c7788225be9ef74c/internal/js_parser/js_parser_lower_class.go#L393-L410

https://github.com/evanw/esbuild/blob/40711afe0baa545012b813c5c7788225be9ef74c/internal/js_parser/js_parser.go#L11343-L11373

Due to the "scan twice" design of esbuild, I'm not sure if it can take further optimizations on this thing. Given these facts:

  1. There's no static fields, so the first condition is not necessary true
  2. There's no static field referencing the class name or this

TypeScript Compiler would give this output:

// input
const decorate = () => void 0
@decorate
class A { #a = A.name }

// output
var A_1;
const decorate = () => void 0;
let A = A_1 = class A {
    #a = A_1.name;
};
A = A_1 = __decorate([
    decorate
], A);

hyrious avatar Feb 20 '24 13:02 hyrious

@evanw As @hyrious mentioned above, in this scenario, there is a difference in the implementation of esbuild and tsc. Private fields still exist, no degradation has occurred. Is this a feature or a bug?

Reference tsc: https://www.typescriptlang.org/play?experimentalDecorators=true&target=9#code/MYewdgzgLgBAJgU1AJwIZQTAvDAFLgSmwD4YBvAXyNQhlTAE8BuAKBYAFEV0EWEAPAA4hksYABsatAMKSItMixgwAxADMAlgnFxsMAESbtcfawpA

const decorate = (() => {}) as any;

@decorate
export class Class {
  #field = "field";
}

/**
 * output

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
const decorate = (() => { });
let Class = class Class {
    #field = "field";
};
Class = __decorate([
    decorate
], Class);
export { Class };

 */

gwsbhqt avatar Feb 26 '24 05:02 gwsbhqt