oxc icon indicating copy to clipboard operation
oxc copied to clipboard

transformer: class constructor argument access modifier special behavior

Open yyx990803 opened this issue 6 months ago • 6 comments

In TypeScript, the following class (case 1) reports a type error:

class Foo {
  x = this.foo // error: Property 'foo' is used before its initialization
  foo: any
  constructor(foo: any) {
    this.foo = foo
  }
}

Because it will be transformed to:

class Foo {
    constructor(foo) {
        this.x = this.foo; // <-- this.foo not assigned yet!
        this.foo = foo;
    }
}

TS Playground

However, if it uses access modifier on the constructor argument (case 2):

class Foo {
  x = this.foo
  constructor(public foo: any) {
    console.log(this.foo)
  }
}

It will work both in types and at runtime, because it will be transformed to:

class Foo {
    constructor(foo) {
        this.foo = foo; // <-- injected from argument
        this.x = this.foo; // <-- moved from field initializers
        console.log(this.foo);
    }
}

TS Playground

Notice how x = this.foo is moved into the constructor to be after the this.foo assignment.

Oxc's current behavior

Oxc currently transforms case 2 to:

class Foo {
  x = this.foo
  constructor(foo) {
    console.log(this.foo)
    this.foo = foo // <-- injected from argument
  }
}

Notice that both x = this.foo and console.log(this.foo) are executed before the this.foo assignment, both leading to runtime errors.

What needs to be fixed

  1. The generated assignment for constructor arguments with access modifiers should be injected to the top of the constructor, not the bottom.
  2. When constructor argument access modifiers are used, all class field initializers need to be moved into the constructor (so their initialization order is preserved), after the argument assignments, and before existing constructor code.

Related: Behavior with useDefineForClassFields: true

When useDefineForClassFields is set to true, case 2 will also throw a type error. And TS's transform out will become:

class Foo {
    foo;
    x = this.foo;
    constructor(foo) {
        this.foo = foo;
        console.log(this.foo);
    }
}

TS Playground

I'm not sure if oxc's TS transform currently takes this into account, but this is something we will need to consider.

yyx990803 avatar Aug 09 '24 13:08 yyx990803