January 26, 2023·4 min read·engineeringtypescriptdecoratorsjavascriptcompiler

TypeScript 5.0 beta introduces a different decorator contract

The beta supports the Stage 3 decorator proposal with target and context arguments. Existing experimental decorators retain a separate checking and emission mode.

Two decorator functions can appear above the same method while expecting different arguments and producing different runtime behavior. TypeScript 5.0 beta makes that distinction immediate. It introduces support for the Stage 3 ECMAScript decorator proposal while preserving the older experimental implementation behind its existing flag.

The January 26 beta announcement describes the new decorator model alongside const type parameters and bundler module resolution. The decorator implementation proposal, merged January 19, specifies the target and context contract. Using decorator syntax without experimentalDecorators no longer means requesting the historical implementation without its flag.

I remain ambivalent about TypeScript, but its framework ecosystem is a strong reason to use it for web interfaces. That makes compatibility between language syntax and framework conventions important. A familiar annotation-like spelling does not establish that a framework decorator supports this new contract.

A method decorator receives the method itself

The new method decorator gets the original function and a context object. It can return a replacement function. Context describes the declaration, including its name and whether it is static or private. TypeScript provides ClassMethodDecoratorContext to represent that information.

A replacement method needs to preserve the calling relationship. The beta announcement shows forwarding this, the argument tuple and the return value. Losing the receiver when calling the original method would change its behavior even if the replacement accepted the same apparent parameters.

The compiler checks both the invocation of the decorator and the consistency of its return value with the decorated target. The implementation description models this through a synthetic call signature. A decorator that returns a method must return one compatible with the method being replaced.

This is more specific than a function that takes arbitrary class metadata. The contract depends on the kind of element. A getter decorator receives its getter function. A setter decorator receives its setter function. Neither receives the combined property descriptor used by the older experimental arrangement.

Fields have another contract because an instance field does not yet exist when the class declaration is being evaluated. Their target argument is undefined. A field decorator can return a function that transforms the initialized value when the field is created. Confusing that timing with method replacement would produce the wrong implementation.

Initializers separate declaration from instance setup

The context object includes addInitializer. A non-static method decorator can register a callback to run during construction, before other field initializers. The announcement uses binding a method to its instance as an example.

Binding matters when a method is passed as a callback and later invoked without its original receiver. The decorator can arrange that binding for each constructed instance without replacing the original method at declaration time. A decorator returning nothing can therefore still change how instances are initialized.

This also explains why a decorator must distinguish static members from instance members. The initializer needs the appropriate object and execution phase. A callback intended to configure each instance is different from one configuring the class itself.

Stacked decorators compose in reverse order. The lower decorator receives the original method, and the decorator above receives its result. Logging around a replacement and binding that replacement can be compatible, but order becomes observable when decorators depend on what earlier replacements return.

Legacy metadata is a separate dependency

The beta retains experimentalDecorators for the old behavior. The announcement says checking and emitted JavaScript differ sufficiently that existing decorator functions are unlikely to support both automatically. Migration therefore depends on the library implementing the decorator, not merely on whether the application compiles its use site.

The new proposal does not support parameter decorators and is incompatible with emitDecoratorMetadata. Those are direct constraints on frameworks that depend on either facility. Their existing implementation can continue using the experimental mode, while adopting the new mode requires an appropriate alternative contract.

I find the explicit separation preferable to assuming that a standardized spelling guarantees compatibility. A framework can build substantial behavior around decorator invocation and emitted metadata. Those dependencies deserve to be visible in its compiler configuration and documentation.

Const inference changes another library boundary

The same beta introduces const type parameters. An API author can request const-like inference for object and array expressions written directly in a call. This moves part of the responsibility for preserving literal information into the generic function declaration.

The announcement distinguishes that behavior from freezing an existing value. A variable already inferred as string[] does not recover a tuple of string literals merely because it is passed to a const generic. The modifier affects inference from the expression available at the call site.

A readonly constraint matters too. If the const candidate is a readonly tuple but the constraint requires a mutable array, inference can fall back to the broader constraint. A declaration using readonly string[] can retain the tuple candidate. The beta therefore gives library authors a useful inference control, while requiring the declared mutability to match the precision they intend to preserve.