What’s New in TC39? Exploring Decorators, Type Annotations & Immutable Arrays
The article reviews recent TC39 proposal updates—including decorators reaching Stage 3, type annotations entering Stage 1, the Change‑Array‑by‑Copy immutable methods, RegExp set notation, decorator metadata, and native once‑only functions—explaining their motivations, new syntax, stage‑gate criteria, and example implementations for JavaScript developers.
In the latest TC39 meeting, the highly discussed decorators proposal and the Type Annotations proposal made progress, moving to Stage 3 and Stage 1 respectively, while no proposal advanced from Stage 3 to Stage 4.
Stage 2 → Stage 3
To advance from Stage 2 to Stage 3, a proposal must:
Produce a complete standard text reviewed and signed off by designated TC39 members.
Obtain the ECMAScript editor’s sign‑off.
Decorators
Proposal link: https://github.com/tc39/proposal-decorators
Decorators are widely used in JavaScript/TypeScript, but the current proposal is now in its third version, which differs from the earlier TypeScript implementation.
A decorator is essentially a function that can dynamically modify a class or class member during initialization or after instantiation.
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 parameter is the class or member 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 and isPrivate : accessibility flags.
addInitializer : allows adding logic that runs when the class is instantiated.
New calling conventions include:
Class expressions can now be decorated, e.g. const Foo = @deco class { constructor() {} } Exported classes can be decorated:
export default @deco class Foo {}Class Decorator
type ClassDecorator = (value: Function, context: { kind: "class"; name: string | undefined; addInitializer(initializer: () => void): void; }) => Function | void;A class decorator can replace the original class by returning a new one or 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 and can replace or augment it.
class C { @foo get x() { /* ... */ } set x(val) { /* ... */ } }Auto Accessor
The proposal also introduces the accessor keyword for auto‑accessors, whose storage, getter, and setter are private. class C { accessor x = 1; } Compiled equivalent:
class C { #x = 1; get x() { return this.#x; } set x(val) { this.#x = val; } }Static and private auto‑accessors can also be decorated.
class C { static accessor x = 1; accessor #y = 2; } type ClassAutoAccessorDecorator = (value: { get(): unknown; set(value: unknown): void; }, context: { kind: "accessor"; name: string | symbol; access: { get(): unknown; set(value: unknown): void; }; isStatic: boolean; isPrivate: boolean; addInitializer(initializer: () => void): void; }) => { get?: () => unknown; set?: (value: unknown) => void; init?: (initialValue: unknown) => unknown; } | void;Decorator Metadata
Metadata is being split from the decorators proposal. It is currently at Stage 2.
Metadata adds a metadataKey and optional class field to the decorator context, allowing access to class‑level metadata.
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') m() {} }
METADATA.get(C[Symbol.metadata]); // 'a'
METADATA.get(C.m[Symbol.metadata]); // 'b'Stage 0 → Stage 1
Advancing from Stage 0 to Stage 1 requires:
A TC39 champion.
A clear problem statement and proposed solution.
Examples illustrating the problem and solution.
Discussion of API shape, algorithms, semantics, and implementation risks.
Type Annotations
Proposal link: https://github.com/tc39/proposal-type-annotations
The proposal aims to add native static type checking to JavaScript, mirroring much of TypeScript’s syntax (type imports/exports, interfaces, type aliases, generics, etc.).
import type { Foo } from "foo";
import type * as Bar from "bar";
let x: string;
x = "hello";
x = 100; // type error
function equals(x: number, y?: number): boolean { return x === y; }
interface Person { name: string; age: number; }
export type CoolBool = boolean;Additional features include type assertions, non‑null assertions, and generic syntax. The proposal introduces a :: token to disambiguate generic calls that would otherwise be parsed as normal function calls.
function foo<T>(x: T) { return x; }
foo::<number>(4, 5); // explicit generic callNot all TypeScript features are planned for inclusion (e.g., declaration files, overloads, private fields). The focus remains on compile‑time, erasable types.
Function.prototype.once
Proposal link: https://github.com/tc39/proposal-function-once
The proposal adds a native once method to create a version of a function that executes only on its first call, returning the cached result on subsequent calls.
function f(x) { console.log(x); return x * 2; }
const fOnce = f.once();
fOnce(3); // logs 3, returns 6
fOnce(3); // does not log, still returns 6Intl.MessageFormat
Proposal link: https://github.com/tc39/proposal-intl-messageformat
The proposal introduces Intl.MessageFormat to support MessageFormat 2.0, enabling standardized localization of messages.
new_notifications [$count] =
[0] 你现在还没有信息
[one] 你收到新信息了~
[_] 你收到了 {$count} 条新信息,快打开看看吧!
const resource = /* MF2 definition */;
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) invites developers to discuss ECMAScript topics on GitHub: https://github.com/JSCIG/es-discuss/discussions .
Taobao Frontend Technology
The frontend landscape is constantly evolving, with rapid innovations across familiar languages. Like us, your understanding of the frontend is continually refreshed. Join us on Taobao, a vibrant, all‑encompassing platform, to uncover limitless potential.
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.
