Decorator field/accessor initializer order

Chris Hewell Garrett

2023-05

  • Field and accessor initializers run from innermost decorator to outermost decorator
  • Getters/setters/methods are replaced from innermost decorator to outermost decorator
  • This means that getters/setters/methods execute from outermost to innermost
  • This makes it impossible currently to have initial values match assigned value for certain accessors
function minusTwo({ set }) {
  return {
    set(v) {
      set.call(this, v - 2)
    },
    init(v) {
      return v - 2;
    }
  }
}

function timesFour({ set }) {
  return {
    set(v) {
      set.call(this, v * 4)
    },
    init(v) {
      return v * 4;
    }
  }
}

class Foo {
  @minusTwo @timesFour accessor bar = 5;
}

const foo = new Foo();
console.log(foo.bar); // 18

foo.bar = 5;
console.log(foo.bar); // 12 

Why is this an issue now?

  • Previously, TypeScript and Babel legacy did run initializers in the current order, innermost -> outermost
  • However, they also assigned the value with [[Set]] semantics to the field, thus running it through all of the setters for the field 
  • The switch to [[Define]] semantics for fields + the redesign of accessors led to this being an issue that wasn't realized until it had been used in the ecosystem for some time

Proposed Solution 1

Reverse the order of initializers, run them from outermost -> innermost

  • Conceptually, we are running the initializers as if the value is being [[Set]] on initialization, so it runs through them in the same order as method calls.
  • This means initialization and method calls always execute from outermost to innermost
  • Decorators still EVALUATE from innermost -> outermost. That does not change.
  • Allows users to distinguish between initial value and updated value, for auto-accessors
class Foo {
  @minusTwo @timesFour accessor bar = 5;
}

// 5 - 2 * 4 = 12

const foo = new Foo();
console.log(foo.bar); // 12

foo.bar = 5;
console.log(foo.bar); // 12 

Proposed Solution 2

Restore the previous behavior, have setters called with initial value for auto-accessors only

  • Most similar to existing behavior in the ecosystem, so less churn overall
  • Can still distinguish between initial and updated value, but requires a flag to do so (e.g. via WeakSet)
  • Only affects auto-accessors, field/method behavior remains the same
class Foo {
  @minusTwo @timesFour accessor bar = 5;
}

// Initial value = 4 * 5 - 2 = 18
// Set value = Initial value - 2 * 4 = 64

const foo = new Foo();
console.log(foo.bar); // 64

foo.bar = 5;
console.log(foo.bar); // 12 

Decorator field/accessor initializer order

By pzuraq

Decorator field/accessor initializer order

  • 300