Skip to content

Class decorators run before class static side is fully defined when downlevelingΒ #61862

Open
@james-pre

Description

@james-pre

πŸ”Ž Search Terms

decorator downlevel
decorator static

πŸ•— Version & Regression Information

Tested on TypeScript 5.6.3, 5.7.3, 5.8.3 with targets ES2022 and ES2024

⏯ Playground Link

https://www.typescriptlang.org/play/?useDefineForClassFields=true&target=11#code/LAKAZgrgdgxgLgSwPZQAQBMCmMkCcCGcmAFHPrgOaZwBcqx+ARgM5wHypSYDu9AdAPIVmdfFACeAbQC6ASlQBeAHyox4+QDJUAb1QAPURNQBfedtCpUOKMyQAbTHztIKxAOQANVAmao3qAGpUMkpqPj1ZAG5QY1BQAAEsHAIiUBg7fGZfAFE9fABbAAcHHQtUVkIEGH1FP3F8cTc4kGMgA

πŸ’» Code

function decorate(target: (abstract new (...args: any[]) => any) & { x: any }) {
  console.log('X is ' + target.x);
}

@decorate
class Example {
  static x = 'yay'
}

πŸ™ Actual behavior

Class decorators are run before the class definition is finished executing. In the example, this results in the log message X is undefined.

πŸ™‚ Expected behavior

As per the the stage 3 decorators proposal:

The result of decorators is stored in the equivalent of local variables to be later called after the class definition initially finishes executing.

Consequently, the example should log the message X is yay

Additional information about the issue

The emitted JS places static initialization blocks before the class body from source:

let Example = (() => {
    let _classDecorators = [decorate];
    let _classDescriptor;
    let _classExtraInitializers = [];
    let _classThis;
    var Example = class {
        static { _classThis = this; }
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
            Example = _classThis = _classDescriptor.value;
            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        static x = 'yay';
        static {
            __runInitializers(_classThis, _classExtraInitializers);
        }
    };
    return Example = _classThis;
})();

A potential solution would be to move these blocks after the source code of the class body, like so:

let Example = (() => {
    let _classDecorators = [decorate];
    let _classDescriptor;
    let _classExtraInitializers = [];
    let _classThis;
    var Example = class { 
        static x = 'yay';
        static { _classThis = this; }
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
            __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
            Example = _classThis = _classDescriptor.value;
            if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        static {
            __runInitializers(_classThis, _classExtraInitializers);
        }
    };
    return Example = _classThis;
})();

Also, this does not occur with the ESNext target since decorators are no longer downleveled.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptHelp WantedYou can do this

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions