typed-reflector
v1.1.2
Published
Metadata reflector with typing support.
Maintainers
Readme
typed-reflector
Typed metadata decorators and metadata reader for TypeScript.
This library provides:
- typed metadata keys and values
- decorator helpers for class, property, method, accessor, and parameter metadata
- metadata lookup with inheritance and prototype traversal
- a built-in metadata registry, without
reflect-metadata - a default global registry for backward compatibility
- optional isolated registries for advanced use cases
- separate
/legacy,/modern, and/universalentries
Installation
npm install typed-reflectorNo reflect-metadata dependency is required.
Entry Points
The package exposes four entry points:
typed-reflector- compatibility alias of
typed-reflector/legacy
- compatibility alias of
typed-reflector/legacy- legacy TypeScript decorators
- built on
MetadataRegistry/GlobalRegistry - supports
param()andgetMetadataFromDecorator()
typed-reflector/modern- standard decorators
- still stores metadata in
MetadataRegistry/GlobalRegistry - uses standard decorator metadata objects only to locate the correct class/member target and inheritance chain
- does not support
param()orgetMetadataFromDecorator()
typed-reflector/universal- one decorator runtime that accepts both legacy and standard decorator calls
- intended for library authors exposing decorators to downstream users
- supports only the API surface shared by both decorator systems
TypeScript Setup
Legacy Entry
Applies to both:
typed-reflectortyped-reflector/legacy
Enable legacy decorators in tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true
}
}emitDecoratorMetadata is not required by this library.
Modern Entry
Use /modern or /universal with standard decorators.
Do not enable experimentalDecorators for this mode.
Example tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "esnext.decorators"]
}
}typed-reflector/modern and typed-reflector/universal automatically ensure Symbol.metadata exists when their modules are loaded, but your toolchain still needs to compile standard decorators and provide context.metadata.
Quick Start
import { Reflector } from 'typed-reflector';
interface MetadataMap {
role: string;
}
interface MetadataArrayMap {
tags: string;
keys: string;
}
const metadata = new Reflector<MetadataMap, MetadataArrayMap>();
const reflector = new Reflector<MetadataMap, MetadataArrayMap>();
@metadata.set('role', 'service')
class UserService {
@metadata.append('tags', 'public', 'keys')
profile!: string;
@metadata.set('role', 'getter')
get currentRole() {
return 'service';
}
}
const service = new UserService();
reflector.get('role', UserService); // "service"
reflector.get('tags', service, 'profile'); // ["public"]
reflector.get('role', service, 'currentRole'); // "getter"
reflector.get('keys', UserService); // ["profile"]Concepts
Type Maps
Reflector<M, AM> accepts two type maps:
M: scalar metadata valuesAM: array element metadata values
Example:
interface MetadataMap {
name: string;
priority: number;
}
interface MetadataArrayMap {
tags: string;
params: number;
}With the definitions above:
set('name', 'x')is allowedset('priority', 1)is allowedappend('tags', 'x')is allowedparam('params', 0)is allowed- wrong key/value combinations fail at compile time
Registry Model
The library stores metadata in a MetadataRegistry.
GlobalRegistryis used by default- this keeps old code working without extra setup
- you can create an isolated registry with
new MetadataRegistry()
Metadata lookup follows the prototype chain in a way similar to reflect-metadata.
Main Exports
import {
Reflector,
MetadataSetter,
MetadataRegistry,
GlobalRegistry,
} from 'typed-reflector';Notes:
Reflectoris the main classMetadataSetteris kept only for API compatibilityMetadataSetteris now just an alias ofReflectortyped-reflectorre-exportstyped-reflector/legacy
The explicit legacy entry exports the same runtime:
import { Reflector, MetadataSetter } from 'typed-reflector/legacy';For standard decorators:
import { MetadataSetter, Reflector } from 'typed-reflector/modern';In /modern, MetadataSetter is also just an alias of Reflector.
For mixed downstream support:
import { Reflector } from 'typed-reflector/universal';Reflector Construction
Default Global Registry
const reflector = new Reflector<MyMetadata, MyArrayMetadata>();This instance reads and writes through GlobalRegistry.
Custom Registry
const registry = new MetadataRegistry();
const metadata = new Reflector<MyMetadata, MyArrayMetadata>({ registry });
const reflector = new Reflector<MyMetadata, MyArrayMetadata>({ registry });Use this when you want metadata isolation between modules, tests, or plugin systems.
Decorator APIs
One Reflector instance can both define metadata and read it back.
const metadata = new Reflector<M, AM>();
const reflector = new Reflector<M, AM>();Using separate variables is optional. They can be the same instance.
set(metadataKey, metadataValue, keysIndexMeta?)
Sets metadata directly and overwrites any inherited or existing value on the current target.
Supported targets:
- class decorators
- property decorators
- method decorators
- accessor decorators
- static members
Example:
class Example {
@metadata.set('name', 'field-name')
field!: string;
@metadata.set('name', 'method-name')
method() {}
@metadata.set('name', 'getter-name')
get value() {
return 1;
}
}If keysIndexMeta is provided for a member decorator, the member key is appended uniquely to that array metadata on the class:
class Example {
@metadata.set('name', 'field-name', 'keys')
field!: string;
}
reflector.get('keys', Example); // ["field"]append(metadataKey, metadataValue, keysIndexMeta?)
Appends one item to an array metadata entry.
class Example {
@metadata.append('tags', 'a', 'keys')
@metadata.append('tags', 'b', 'keys')
field!: string;
}
reflector.get('tags', Example, 'field'); // ["a", "b"]appendUnique(metadataKey, metadataValue, keysIndexMeta?)
Same as append, but avoids duplicates.
class Example {
@metadata.appendUnique('tags', 'a')
@metadata.appendUnique('tags', 'a')
field!: string;
}
reflector.get('tags', Example, 'field'); // ["a"]concat(metadataKey, metadataValue, keysIndexMeta?)
Concatenates an array into existing array metadata.
class Example {
@metadata.concat('tags', ['a', 'b'])
field!: string;
}param(metadataKey, metadataValue, keysIndexMeta?)
Writes array metadata by parameter index.
Supported targets:
- constructor parameters
- method parameters
- static method parameters
Example:
class Example {
constructor(
@metadata.param('params', 10) first: string,
@metadata.param('params', 20) second: string,
) {}
method(
@metadata.param('params', 30, 'keys') a: string,
@metadata.param('params', 40, 'keys') b: string,
) {}
}
reflector.get('params', Example); // [10, 20]
reflector.get('params', Example, 'method'); // [30, 40]
reflector.get('keys', Example); // ["method"]Important:
- constructor parameter decorators are supported through
param() keysIndexMetais only indexed for named members- constructor parameters do not add a key index because the constructor has no property key
transform(metadataKey, updater, keysIndexMeta?)
Low-level helper used by the other decorator methods.
It receives the current metadata value and returns the next value.
class Example {
@metadata.transform('priority', (oldValue) => (oldValue || 0) + 1)
field!: string;
}Read APIs
get(metadataKey, instanceOrClass, key?)
Reads metadata from:
- a class
- an instance
- a member on a class
- a member on an instance
Example:
@metadata.set('name', 'base')
class Base {
@metadata.set('name', 'field')
field!: string;
}
const base = new Base();
reflector.get('name', Base); // "base"
reflector.get('name', base); // "base"
reflector.get('name', Base, 'field'); // "field"
reflector.get('name', base, 'field'); // "field"Member lookup checks both the constructor side and the prototype side, then traverses inheritance.
That means these cases work as expected:
- instance fields and methods
- accessors
- static methods and static properties
- inherited metadata from parent classes or parent prototypes
getArray(metadataKey, instanceOrClass, key?)
Same as get, but returns [] when no value exists.
reflector.getArray('tags', Example, 'field');getProperty(metadataKey, instanceOrClass, key, alternate?)
Merges array metadata from:
- class-level metadata on the current target
- class-level metadata on
alternateif provided - member-level metadata on
key
Example:
@metadata.append('tags', 'class-tag')
class Example {
@metadata.append('tags', 'field-tag')
field!: string;
}
reflector.getProperty('tags', Example, 'field');
// ["class-tag", "field-tag"]This API is useful when class-level array metadata acts like a default set, and member-level array metadata adds local detail.
getMetadataFromDecorator(decorator, metadataKey, options?)
Extracts metadata produced by a decorator function by applying it to a temporary target.
This is useful when you want to inspect metadata from a decorator factory without defining a real class just for that check.
Examples:
const classLikeDecorator = metadata.set('name', 'demo');
const methodLikeDecorator = metadata.append('tags', 'x');
const constructorParamDecorator = metadata.param('params', 1);
const methodParamDecorator = metadata.param('params', 2);
reflector.getMetadataFromDecorator(classLikeDecorator, 'name'); // "demo"
reflector.getMetadataFromDecorator(methodLikeDecorator, 'tags'); // ["x"]
reflector.getMetadataFromDecorator(
constructorParamDecorator,
'params',
{ index: 0 },
); // [1]
reflector.getMetadataFromDecorator(
methodParamDecorator,
'params',
{ key: 'method', index: 0 },
); // [2]Options:
key: inspect member metadata instead of constructor/class-level metadataindex: treat the decorator as a parameter decorator
MetadataRegistry API
MetadataRegistry is the low-level storage abstraction.
const registry = new MetadataRegistry();Available methods:
defineMetadata(metadataKey, value, target, propertyKey?)getMetadata(metadataKey, target, propertyKey?)getOwnMetadata(metadataKey, target, propertyKey?)hasMetadata(metadataKey, target, propertyKey?)hasOwnMetadata(metadataKey, target, propertyKey?)
Example:
class Base {}
class Child extends Base {}
registry.defineMetadata('role', 'base', Base);
registry.getMetadata('role', Child); // "base"
registry.getOwnMetadata('role', Child); // undefined
registry.hasMetadata('role', Child); // true
registry.hasOwnMetadata('role', Child); // falseInheritance Behavior
Metadata is inherited during lookup.
@metadata.set('name', 'base')
class Base {
@metadata.set('name', 'base-field')
field!: string;
}
class Child extends Base {}
reflector.get('name', Child); // "base"
reflector.get('name', Child, 'field'); // "base-field"If a child defines its own metadata, that value overrides inherited lookup for that exact target.
Accessor Decorators
Accessor decorators are supported through the same APIs used for methods and properties:
class Example {
@metadata.set('name', 'getter')
get value() {
return 1;
}
@metadata.append('tags', 'setter')
set value(input: number) {
void input;
}
}Lookup works with either the class or an instance:
reflector.get('name', Example, 'value');
reflector.get('name', new Example(), 'value');Built-in Safety
The registry stores metadata in a WeakMap and does not patch built-in JavaScript objects such as:
ObjectFunctionObject.prototypeFunction.prototype
This avoids the global prototype pollution problems that metadata systems sometimes cause.
Backward Compatibility
Old code like this still works:
import { MetadataSetter, Reflector } from 'typed-reflector';
const Metadata = new MetadataSetter<M, AM>();
const reflector = new Reflector<M, AM>();MetadataSetter is retained as a compatibility export, but new code should prefer Reflector.
typed-reflector now behaves as a compatibility alias of typed-reflector/legacy.
/legacy
typed-reflector/legacy is the explicit legacy-decorators entry.
It exports the same API as typed-reflector, but makes the runtime choice explicit when you want to document or migrate between multiple decorator modes.
/modern
typed-reflector/modern is the standard-decorators entry.
It is intended for projects using the current decorator proposal rather than legacy TypeScript decorators.
/modern Exports
import {
Reflector,
MetadataSetter,
MetadataRegistry,
GlobalRegistry,
ensureSymbolMetadata,
} from 'typed-reflector/modern';Notes:
Reflectoris the main classMetadataSetteris a compatibility alias ofReflectorMetadataRegistryandGlobalRegistryare re-exported for compatibility and shared low-level accessensureSymbolMetadata()is exported in case you want to initializeSymbol.metadataexplicitly before other decorator code runs
/modern Construction
const metadata = new Reflector<M, AM>();
const reflector = new Reflector<M, AM>();By default, /modern uses GlobalRegistry, just like the legacy entry.
You can also provide a custom registry:
import { MetadataRegistry, Reflector } from 'typed-reflector/modern';
const registry = new MetadataRegistry();
const metadata = new Reflector<M, AM>({ registry });
const reflector = new Reflector<M, AM>({ registry });/modern stores metadata values in MetadataRegistry, just like the legacy entry.
It still depends on standard decorator metadata support at runtime:
- decorators must receive
context.metadata - class metadata objects must be reachable through
Symbol.metadata
That metadata object is used only to locate the correct registry target and inheritance chain. The library does not store its own metadata values inside context.metadata.
/modern Supported APIs
The modern entry supports the same read APIs as the legacy entry:
get()getArray()getProperty()
It also supports these decorator factories:
set()append()appendUnique()concat()transform()
/modern Unsupported APIs
These legacy-only APIs are not available in /modern:
param()getMetadataFromDecorator()
The main limitation is that standard decorators do not support parameter decorators, so constructor parameter metadata and method parameter metadata are intentionally excluded from /modern.
/modern Example
import { MetadataSetter, Reflector } from 'typed-reflector/modern';
interface MetadataMap {
role: string;
}
interface MetadataArrayMap {
tags: string;
keys: string;
}
const metadata = new MetadataSetter<MetadataMap, MetadataArrayMap>();
const reflector = new Reflector<MetadataMap, MetadataArrayMap>();
@metadata.set('role', 'service')
class Example {
@metadata.append('tags', 'field', 'keys')
field = 'value';
@metadata.set('role', 'method')
method() {}
@metadata.set('role', 'getter')
get currentRole() {
return 'service';
}
@metadata.append('tags', 'static')
static boot() {}
}
const instance = new Example();
reflector.get('role', Example); // "service"
reflector.get('tags', instance, 'field'); // ["field"]
reflector.get('role', instance, 'method'); // "method"
reflector.get('role', instance, 'currentRole'); // "getter"
reflector.get('tags', Example, 'boot'); // ["static"]
reflector.get('keys', Example); // ["field"]/modern Inheritance
Metadata lookup in /modern follows the registry targets derived from the Symbol.metadata prototype chain.
That means parent class metadata is visible from child classes and child instances:
import { Reflector } from 'typed-reflector/modern';
interface MetadataMap {
name: string;
}
interface MetadataArrayMap {
keys: string;
}
const metadata = new Reflector<MetadataMap, MetadataArrayMap>();
const reflector = new Reflector<MetadataMap, MetadataArrayMap>();
@metadata.set('name', 'base')
class Base {
@metadata.set('name', 'base-field', 'keys')
field = 'value';
}
class Child extends Base {}
reflector.get('name', Child); // "base"
reflector.get('name', Child, 'field'); // "base-field"
reflector.get('keys', Child); // ["field"]/modern Authoring Notes
- Use one metadata decorator per key when possible. Relying on decorator stacking order for the same key can make tests and consumer code harder to reason about.
/moderndecorators only work in standard decorator mode. If they are called like legacy decorators, the library throws aTypeError.- If you are building helpers on top of
/modern, prefer the types exported bytyped-reflector/modern, such asModernDecorators, instead of the legacy decorator types.
/universal
typed-reflector/universal is intended for libraries that expose decorators to downstream users and do not want to force either legacy or standard decorator syntax at the publishing boundary.
It uses the same MetadataRegistry / GlobalRegistry storage model as the other entries, and dispatches at runtime based on how the decorator is invoked.
/universal Exports
import {
Reflector,
MetadataSetter,
MetadataRegistry,
GlobalRegistry,
ensureSymbolMetadata,
} from 'typed-reflector/universal';Notes:
MetadataSetteris a compatibility alias ofReflectorensureSymbolMetadata()has the same purpose as in/modernUniversalDecoratorsis exported fromtyped-reflector/universalfor helper libraries
/universal Supported APIs
The universal entry supports only the common subset shared by legacy and standard decorators:
set()append()appendUnique()concat()transform()get()getArray()getProperty()
/universal Unsupported APIs
These APIs are intentionally omitted because they are not portable across both decorator systems:
param()getMetadataFromDecorator()
/universal Use Case
import { Reflector } from 'typed-reflector/universal';
interface MetadataMap {
kind: string;
}
interface MetadataArrayMap {
tags: string;
keys: string;
}
const metadata = new Reflector<MetadataMap, MetadataArrayMap>();
export function Column() {
return metadata.set('kind', 'column');
}
export function Public() {
return metadata.append('tags', 'public', 'keys');
}This is mainly useful when a library wants to export @Column()-style decorators and let the final application decide whether it is using legacy decorators or standard decorators.
Complete Example
import { MetadataRegistry, Reflector } from 'typed-reflector';
interface MetadataMap {
kind: string;
}
interface MetadataArrayMap {
tags: string;
params: string;
keys: string;
}
const registry = new MetadataRegistry();
const metadata = new Reflector<MetadataMap, MetadataArrayMap>({ registry });
const reflector = new Reflector<MetadataMap, MetadataArrayMap>({ registry });
@metadata.set('kind', 'service')
@metadata.append('tags', 'injectable')
class UserService {
@metadata.append('tags', 'field', 'keys')
repo!: unknown;
constructor(
@metadata.param('params', 'id') id: string,
@metadata.param('params', 'token') token: string,
) {
void id;
void token;
}
@metadata.set('kind', 'resolver')
resolve(
@metadata.param('params', 'userId', 'keys') userId: string,
@metadata.param('params', 'traceId', 'keys') traceId: string,
) {
void userId;
void traceId;
}
@metadata.set('kind', 'getter')
get state() {
return 'ready';
}
}
const service = new UserService('1', 'secret');
reflector.get('kind', UserService); // "service"
reflector.get('tags', UserService); // ["injectable"]
reflector.get('tags', service, 'repo'); // ["field"]
reflector.get('params', UserService); // ["id", "token"]
reflector.get('params', service, 'resolve'); // ["userId", "traceId"]
reflector.get('kind', service, 'state'); // "getter"
reflector.get('keys', UserService); // ["repo", "resolve"]
reflector.getProperty('tags', service, 'repo'); // ["injectable", "field"]License
MIT
