Skip to content

transformer: class constructor argument access modifier special behavior #4789

@yyx990803

Description

@yyx990803

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.

Metadata

Metadata

Assignees

Labels

A-transformerArea - Transformer / TranspilerC-bugCategory - Bug

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions