This library bridges the gap between TC39 Proposal Decorators (Stage 3) and TypeScript Experimental Decorators, allowing you to write decorators that work in both environments.
It provides a compatibility layer for:
- Unified Decorator Definition: Define a single decorator that handles both standard and experimental behaviors.
- Metadata Reflection: A
reflect-metadatacompatible API (getMetadata,hasMetadata,getMetadataKeys) that works with the new metadata proposal. - Metadata Inheritance: seamless inheritance of metadata values, complying with the proposal spec.
defineDecorator allows you to define the implementation for both ECMAScript (Standard) decorators and TypeScript
Experimental decorators in one place.
import { defineDecorator } from "@denostack/universal-decorator";
const myDecorator = defineDecorator({
// Implementation for TC39 Stage 3 Decorators
ecma(target, ctx) {
console.log(`[ECMA] Decorating ${ctx.kind}: ${String(ctx.name)}`);
// Logic for standard decorators...
},
// Implementation for TypeScript Experimental Decorators
tsExperimental(target, property, descriptor) {
console.log(`[TS] Decorating ${property ? String(property) : "class"}`);
// Logic for experimental decorators...
},
});
// Usage
@myDecorator
class MyClass {
@myDecorator
method() {}
}This library provides a metadata API inspired by reflect-metadata, but adapted for the modern decorator context
structure.
The functions getMetadata, hasMetadata, and getMetadataKeys follow the usage pattern of reflect-metadata.
However, when specifying a property, they expect an object structure { name: string | symbol, static: boolean } which
aligns with the context object in the TC39 decorator proposal.
import { defineMetadataDecorator, getMetadata } from "@denostack/universal-decorator";
// Define metadata
@defineMetadataDecorator("role", "admin")
class User {
@defineMetadataDecorator("permissions", ["read", "write"])
static defaultPermissions: string[];
@defineMetadataDecorator("visible", true)
getName() {}
}
// Retrieve metadata
// Class metadata
const classRole = getMetadata("role", User); // "admin"
// Static property metadata
// Note: Property identifier is an object { name, static }
const staticPerms = getMetadata("permissions", User, { name: "defaultPermissions", static: true }); // ["read", "write"]
// Instance property metadata
const methodVisible = getMetadata("visible", User, { name: "getName", static: false }); // truedefineMetadataDecorator creates a decorator specifically for attaching metadata. It supports metadata inheritance,
meaning metadata defined on a parent class is accessible to child classes unless overridden.
You can also inherit and extend existing metadata values using a callback function: (previousValue) => newValue.
import { defineMetadataDecorator, getMetadata } from "@denostack/universal-decorator";
// 1. Simple Definition
@defineMetadataDecorator("custom:tag", "v1")
class Parent {}
// 2. Inheritance & Extension
// The callback receives the metadata value from the parent class (if any)
@defineMetadataDecorator("custom:tag", (parentValue) => `${parentValue} -> v2`)
class Child extends Parent {}
console.log(getMetadata("custom:tag", Parent)); // "v1"
console.log(getMetadata("custom:tag", Child)); // "v1 -> v2"This inheritance mechanism follows the behavior specified in the Decorator Metadata proposal, ensuring that metadata flows down the prototype chain correctly.
While reflect-metadata uses (metadataKey, target, propertyKey):
- reflect-metadata:
Reflect.getMetadata("key", Target.prototype, "methodName") - universal-decorator:
getMetadata("key", Target, { name: "methodName", static: false })
This library consciously adopts the { name, static } structure to be forwards-compatible with the context object
provided by standard decorators. Unlike reflect-metadata, getOwnMetadata is not strictly separated; instead,
metadata resolution follows the prototype chain (inheritance) by default, as per the proposal.