Skip to content

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

@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 TypeScriptDomain: DecoratorsThe issue relates to the decorator syntaxHelp WantedYou can do this

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions