Explore the Latest TC39 Proposals: Decorators, Type Annotations, and Immutable Array Methods
This article reviews recent TC39 proposal progress—including decorators reaching Stage 3, type annotations advancing to Stage 1, new immutable array methods, RegExp set notation, decorator metadata, and function.once—detailing their syntax, semantics, migration considerations, and example implementations for modern JavaScript development.
In the recent TC39 meeting, the decorator proposal advanced to Stage 3 and the Type Annotations proposal entered Stage 1; no proposal moved from Stage 3 to Stage 4.
Stage 2 → Stage 3 Requirements
To promote a proposal from Stage 2 to Stage 3, the following must be satisfied:
A complete standard text is written, reviewed, and signed off by designated TC39 members.
The ECMAScript editor signs off on the proposal.
Decorators
Decorator syntax is widely used in JavaScript/TypeScript. The current proposal is the third iteration and differs from the legacy TypeScript implementation.
A decorator is a function that can modify a class or class member at definition time.
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: {
get?(): unknown;
set?(value: unknown): void;
};
isPrivate?: boolean;
isStatic?: boolean;
addInitializer?(initializer: () => void): void;
}) => Output | void;The value argument is the target being decorated; context provides metadata such as kind, name, access, isStatic, isPrivate, and addInitializer.
kind : type of the decorated element (class, method, field, etc.).
name : name of the element.
access : getter/setter for the element.
isStatic / isPrivate : visibility flags.
addInitializer : registers code to run during class instantiation.
New calling conventions include allowing decorators on class expressions and adjusting the syntax when used with export:
const Foo = @deco class {};
export default @deco class Bar {};Class Decorator
type ClassDecorator = (value: Function, context: {
kind: "class";
name: string | undefined;
addInitializer(initializer: () => void): void;
}) => Function | void;A class decorator can replace the class entirely or return a subclass:
function logged(value, {kind, name}) {
if (kind === "class") {
return class extends value {
constructor(...args) { super(...args); }
};
}
}
@logged
class C {}Class Method Decorator
type ClassMethodDecorator = (value: Function, context: {
kind: "method";
name: string | symbol;
access: { get(): unknown };
isStatic: boolean;
isPrivate: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;Method decorators can replace the method or wrap it to add extra logic:
function logged(value, {kind, name}) {
if (kind === "method") {
return function(...args) {
const ret = value.call(this, ...args);
return ret;
};
}
}
class C {
@logged
m(arg) {}
}Class Accessor Decorator
type ClassGetterDecorator = (value: Function, context: {
kind: "getter";
name: string | symbol;
access: { get(): unknown };
isStatic: boolean;
isPrivate: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;
type ClassSetterDecorator = (value: Function, context: {
kind: "setter";
name: string | symbol;
access: { set(value: unknown): void };
isStatic: boolean;
isPrivate: boolean;
addInitializer(initializer: () => void): void;
}) => Function | void;Getter and setter decorators receive the original accessor functions and may replace them.
class C {
@foo
get x() { /* ... */ }
set x(val) { /* ... */ }
}Class Field Decorator
type ClassFieldDecorator = (value: undefined, context: {
kind: "field";
name: string | symbol;
access: { get(): unknown; set(value: unknown): void };
isStatic: boolean;
isPrivate: boolean;
}) => (initialValue: unknown) => unknown | void;Field decorators receive undefined as value; they can return a function that transforms the initial value.
function logged(value, {kind, name}) {
if (kind === "field") {
return function(initialValue) {
return 599;
};
}
}
class C {
@logged x = 1;
}
new C().x; // 599Auto Accessor
The proposal introduces the accessor keyword for auto‑accessors, which store values in a private slot and expose getter/setter semantics.
class C {
accessor x = 1;
}Compiled equivalent:
class C {
#x = 1;
get x() { return this.#x; }
set x(v) { this.#x = v; }
}Decorator Metadata
A separate metadata proposal (Stage 2) adds a metadataKey to decorator contexts, enabling storage of metadata on classes and members.
interface MetadataKey { parent: MetadataKey | null; }
type Decorator = (value: Input, context: {
kind: string;
name: string | symbol;
access: { get?(): unknown; set?(value: unknown): void };
isPrivate?: boolean;
isStatic?: boolean;
addInitializer?(initializer: () => void): void;
metadataKey?: MetadataKey;
class?: { metadataKey: MetadataKey; name: string };
}) => Output | void;Example usage:
const METADATA = new WeakMap();
function meta(value) {
return (_, context) => {
METADATA.set(context.metadataKey, value);
};
}
@meta('a')
class C {
@meta('b')
foo;
@meta('c')
get bar() {}
@meta('d')
baz() {}
}
METADATA.get(C[Symbol.metadataKey]); // class metadata
METADATA.get(C.foo[Symbol.metadata]); // 'b'Stage 1 → Stage 2 Transition
Advancing from Stage 1 to Stage 2 requires a complete draft specification, a champion TC39 member, clear problem statement, examples, and analysis of API, algorithms, and risks.
Change Array by Copy
This proposal adds immutable counterparts to mutating array methods:
Array.prototype.toReversed
Array.prototype.toSorted
Array.prototype.toSpliced
Array.prototype.with
TypedArray prototypes receive the same methods. Polyfills are available via CoreJS or ES‑Shims.
RegExp Set Notation
Introduces & syntax for set operations inside character classes, enabling intersection, difference, and nesting:
// Difference
[A--B]
// Intersection
[A&&B]
// Nested set
[A--[0-9]]Examples:
[\p{Decimal_Number}--[0-9]]
[\p{Emoji}--\p{ASCII}]Type Annotations
The most discussed proposal adds native static type syntax to JavaScript, mirroring TypeScript. It includes basic type annotations, interfaces, type imports/exports, type aliases, type assertions, generics, and the :: syntax to disambiguate generic calls.
import type { Foo } from "foo";
let x: string;
function equals(x: number, y?: number): boolean { return x === y; }
interface Person { name: string; age: number; }
export type CoolBool = boolean;Only core type features are targeted; advanced TypeScript constructs like declaration files, overloads, and private fields are out of scope for now.
Function.prototype.once
This proposal adds a native once method to functions, returning a version that executes only on the first call and thereafter returns the cached result.
function f(x) { console.log(x); return x * 2; }
const fOnce = f.once();
fOnce(3); // logs 3, returns 6
fOnce(3); // no log, returns 6Intl.MessageFormat
Extends the Intl API with Intl.MessageFormat to support MessageFormat 2.0, enabling locale‑aware message selection and formatting.
const resource = {
new_notifications: {
0: "你现在还没有信息",
one: "你收到新信息了~",
_: "你收到了 { $count } 条新信息,快打开看看吧!"
}
};
const mf = new Intl.MessageFormat(resource, ['en']);
const msg = mf.resolveMessage('new_notifications', { count: 3 });
msg.toString(); // "你收到了 3 条新信息,快打开看看吧!"Conclusion
The JavaScript Chinese Interest Group (JSCIG) continues to host discussions on ECMAScript proposals. Interested developers are encouraged to join the conversation on GitHub: https://github.com/JSCIG/es-discuss/discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
