Decorator Metadata for Stage 3

Chris Hewell Garrett

2023-05

Refresher: Why metadata?

  • Use cases
    • ORMs (extremely useful to track database column information and size, foreign and primary key relationships, constraints, etc.)
    • Runtime Type Information (often used by DI and ORM, but also for runtime type checking, validation)
    • Serialization/Marshalling (i.e., controlling JSON.stringify output, or WASM or native platform interop)
    • Unit Testing (mark test methods on a class that serves as a test suite, specify related information such as execution contexts, issue numbers, etc.) MSTest and NUnit are good examples of this in C#/.NET
    • Routing (i.e., mark methods as GET or POST, or belonging to a specific url path, or what request body transforms to use)
    • Debugging (i.e., mark classes/methods as something the debugger should step over when debugging, or what alternative representation should be used in a watch/locals window)
    • Dependency Injection
    • Membranes (i.e., mark how certain methods, parameters, or return values should be interpreted when being passed in and out of a membrane-wrapped object)
  • Reflect.metadata is the single most used decorator library (> 5 mil monthly downloads), suggests users find the pattern valuable

How it used to work

Legacy decorators used to receive the class definition as the first argument. Decorators could use the class as a key in a WeakMap to expose metadata.

  • Every decorator had to define its own way of adding metadata.
  • No support for adding metadata to private elements.
  • No longer possible in current proposal because decorators do not receive the class.
const MY_META = new WeakMap();

class Metadata {
  public = {};
  own;
}

function addMeta(value) {
  return function(Class, name, desc) {
    let metadata = MY_META.get(Class);

    if (!metadata) {
      metadata = new Metadata();
      MY_META.set(Class, metadata);
    }

    if (name) {
      metadata.public[name] = value;
    } else {
      metadata.own = value;
    }
  } 
}

@addMeta('class')
class C {
  @addMeta('pub') x;
}

const metadata = MY_META.get(C);

metadata.own; 
// 'class'
metadata.public.x; 
// 'pub'

Current Proposal: Shared Metadata Object

Decorator context receives an additional `metadata` property, which is a POJO.

  • All decorators applied to the class receive the same object
  • The object is assigned to the Symbol.metadata property after class definition
  • Metadata object inherits from parent class's metadata
  • Pros:
    • Very simple
    • Can do shared/public metadata and truly private metadata via WeakMap
    • Inheritance by default, but can opt-out by manually crawling prototype chain
  • Cons
    • Introduces a shared namespace
function meta(key, value) {
  return (_, context) => {
    context.metadata[key] = value;
  };
}

@meta('a' 'x')
class C {
  @meta('b', 'y')
  m() {}
}

C[Symbol.metadata].a; // 'x'
C[Symbol.metadata].b; // 'y'

class D extends C {
  @meta('b', 'z')
  m() {}
}

D[Symbol.metadata].a; // 'x'
D[Symbol.metadata].b; // 'z'

Stage 3?

Decorator Metadata for Stage 3

By pzuraq

Decorator Metadata for Stage 3

  • 863