Metadata

The metadata module provides a low-level API for storing and retrieving metadata on classes, methods, and parameters using the reflect-metadata library. This system forms the foundation for decorators like @register, @inject, @onConstruct, and @onDispose.

Metadata is data about your code that can be attached to classes, methods, and parameters at design time. The TypeScript compiler with reflect-metadata enables reading and writing this metadata at runtime. The metadata system allows you to:

This library provides a simplified, functional API over reflect-metadata that supports accumulation patterns through mapper functions.

To use the metadata system, you need:

[!IMPORTANT]

reflect-metadata is an optional consumer dependency. The runtime package stays dependency-free, so applications using metadata decorators must install and initialize it themselves.

TypeScript
// At application entry point
import 'reflect-metadata';

// In tsconfig.json
{
"compilerOptions": {
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}
}
// At application entry point
import 'reflect-metadata';

// In tsconfig.json
{
"compilerOptions": {
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}
}

The metadata module exports functions organized in pairs per target type:

Class metadata is stored on the class constructor itself. Use this to attach configuration, tags, or registration information to classes.

classMeta

Creates a class decorator that stores metadata using a mapper function. The mapper receives the previous value (if any) and returns the new value.

classMeta<T>(
  key: string | symbol,
  mapFn: (prev: T | undefined) => T
): ClassDecorator

getClassMeta

Retrieves metadata from a class. Accepts either the constructor or an instance — if an instance is passed, the constructor is resolved automatically.

getClassMeta<T>(
  target: object,
  key: string | symbol
): T | undefined

classLabel

Attaches a key/value label to a class. Labels are stored as a Map<string, string> and can be used to categorize or describe classes.

classLabel(key: string, label: string): ClassDecorator

getClassLabels

Retrieves all labels attached to a class. Accepts either the constructor or an instance.

getClassLabels(target: object): Map<string, string>

classTag

Attaches a string tag to a class. Tags are stored as a Set<string>, so duplicates are ignored automatically.

classTag(tag: string): ClassDecorator

getClassTags

Retrieves all tags attached to a class. Accepts either the constructor or an instance.

getClassTags(target: object): Set<string>

Example

TypeScript
const TAGS_KEY = 'tags';

@classMeta(TAGS_KEY, (prev: string[] = []) => [...prev, 'service'])
@classMeta(TAGS_KEY, (prev: string[] = []) => [...prev, 'api'])
class ApiService {}

const tags = getClassMeta<string[]>(ApiService, TAGS_KEY);
console.log(tags); // ['api', 'service']
const TAGS_KEY = 'tags';

@classMeta(TAGS_KEY, (prev: string[] = []) => [...prev, 'service'])
@classMeta(TAGS_KEY, (prev: string[] = []) => [...prev, 'api'])
class ApiService {}

const tags = getClassMeta<string[]>(ApiService, TAGS_KEY);
console.log(tags); // ['api', 'service']
TypeScript
@classTag('singleton')
@classTag('service')
@classLabel('env', 'production')
@classLabel('region', 'us-east')
class MyService {}

const instance = new MyService();

// Works with both constructor and instance
console.log(getClassTags(MyService));          // Set { 'service', 'singleton' }
console.log(getClassTags(instance));           // Set { 'service', 'singleton' }
console.log(getClassLabels(MyService).get('env'));    // 'production'
console.log(getClassLabels(instance).get('region')); // 'us-east'
@classTag('singleton')
@classTag('service')
@classLabel('env', 'production')
@classLabel('region', 'us-east')
class MyService {}

const instance = new MyService();

// Works with both constructor and instance
console.log(getClassTags(MyService));          // Set { 'service', 'singleton' }
console.log(getClassTags(instance));           // Set { 'service', 'singleton' }
console.log(getClassLabels(MyService).get('env'));    // 'production'
console.log(getClassLabels(instance).get('region')); // 'us-east'

Parameter metadata is stored as an array indexed by parameter position. This is crucial for constructor and method parameter injection.

paramMeta

Creates a parameter decorator that stores metadata for a specific parameter. The mapper receives the previous value for that parameter index.

paramMeta(
  key: string | symbol,
  mapFn: (prev: unknown) => unknown
): ParameterDecorator

getParamMeta

Retrieves parameter metadata as an array. Undecorated parameters will have undefined at their index. Accepts either the constructor or an instance.

getParamMeta(
  key: string | symbol,
  target: object
): unknown[]

paramLabel

Attaches a key/value label to a constructor parameter at the given position. Labels are stored per parameter as a Map<string, string>.

paramLabel(key: string, label: string): ParameterDecorator

getParamLabels

Retrieves all labels for a specific parameter by index. Accepts either the constructor or an instance.

getParamLabels(target: object, parameterIndex: number): Map<string, string>

paramTag

Attaches a string tag to a constructor parameter. Tags are stored per parameter as a Set<string>.

paramTag(tag: string): ParameterDecorator

getParamTags

Retrieves all tags for a specific parameter by index. Accepts either the constructor or an instance.

getParamTags(target: object, parameterIndex: number): Set<string>

Example

TypeScript
const INJECT_KEY = 'inject:constructor';

class DatabaseService {
constructor(
  @paramMeta(INJECT_KEY, () => 'config') config: any,
  @paramMeta(INJECT_KEY, () => 'logger') logger: any,
) {}
}

const metadata = getParamMeta(INJECT_KEY, DatabaseService);
console.log(metadata); // ['config', 'logger']
const INJECT_KEY = 'inject:constructor';

class DatabaseService {
constructor(
  @paramMeta(INJECT_KEY, () => 'config') config: any,
  @paramMeta(INJECT_KEY, () => 'logger') logger: any,
) {}
}

const metadata = getParamMeta(INJECT_KEY, DatabaseService);
console.log(metadata); // ['config', 'logger']
TypeScript
class MyService {
constructor(
  @paramTag('optional')
  @paramLabel('source', 'env')
  _config: unknown,

  @paramTag('required')
  @paramLabel('source', 'di')
  _db: unknown,
) {}
}

// Works with both constructor and instance
console.log(getParamTags(MyService, 0).has('optional'));       // true
console.log(getParamLabels(MyService, 1).get('source'));       // 'di'
console.log(getParamTags(new MyService(null, null), 1).has('required')); // true
class MyService {
constructor(
  @paramTag('optional')
  @paramLabel('source', 'env')
  _config: unknown,

  @paramTag('required')
  @paramLabel('source', 'di')
  _db: unknown,
) {}
}

// Works with both constructor and instance
console.log(getParamTags(MyService, 0).has('optional'));       // true
console.log(getParamLabels(MyService, 1).get('source'));       // 'di'
console.log(getParamTags(new MyService(null, null), 1).has('required')); // true

Method metadata is stored per method on the class. Use this for hooks, validators, middleware, or any method-level configuration.

methodMeta

Creates a method decorator that stores metadata for a specific method. The mapper receives the previous value for that method.

methodMeta<T>(
  key: string,
  mapFn: (prev: T | undefined) => T
): MethodDecorator

getMethodMeta

Retrieves metadata for a specific method. Accepts either the constructor or an instance.

getMethodMeta(
  key: string,
  target: object,
  propertyKey: string
): unknown

methodLabel

Attaches a key/value label to a method. Labels are stored as a Map<string, string>.

methodLabel(key: string, label: string): MethodDecorator

getMethodLabels

Retrieves all labels attached to a method. Accepts either the constructor or an instance.

getMethodLabels(target: object, propertyKey: string): Map<string, string>

methodTag

Attaches a string tag to a method. Tags are stored as a Set<string>, so duplicates are ignored automatically.

methodTag(tag: string): MethodDecorator

getMethodTags

Retrieves all tags attached to a method. Accepts either the constructor or an instance.

getMethodTags(target: object, propertyKey: string): Set<string>

Example

TypeScript
const MIDDLEWARE_KEY = 'middleware';

class Controller {
@methodMeta(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'auth'])
@methodMeta(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'validate'])
handleRequest() {}
}

const controller = new Controller();
const middleware = getMethodMeta(MIDDLEWARE_KEY, controller, 'handleRequest');
console.log(middleware); // ['validate', 'auth']
const MIDDLEWARE_KEY = 'middleware';

class Controller {
@methodMeta(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'auth'])
@methodMeta(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'validate'])
handleRequest() {}
}

const controller = new Controller();
const middleware = getMethodMeta(MIDDLEWARE_KEY, controller, 'handleRequest');
console.log(middleware); // ['validate', 'auth']
TypeScript
class UserController {
@methodTag('public')
@methodTag('deprecated')
@methodLabel('version', 'v1')
getUsers() {}

@methodTag('public')
@methodLabel('version', 'v2')
getUsersV2() {}
}

const ctrl = new UserController();

// Works with both constructor and instance
console.log(getMethodTags(UserController, 'getUsers').has('deprecated')); // true
console.log(getMethodLabels(ctrl, 'getUsers').get('version'));             // 'v1'
console.log(getMethodTags(ctrl, 'getUsersV2').has('deprecated'));          // false
class UserController {
@methodTag('public')
@methodTag('deprecated')
@methodLabel('version', 'v1')
getUsers() {}

@methodTag('public')
@methodLabel('version', 'v2')
getUsersV2() {}
}

const ctrl = new UserController();

// Works with both constructor and instance
console.log(getMethodTags(UserController, 'getUsers').has('deprecated')); // true
console.log(getMethodLabels(ctrl, 'getUsers').get('version'));             // 'v1'
console.log(getMethodTags(ctrl, 'getUsersV2').has('deprecated'));          // false

The metadata system is built on top of reflect-metadata:

The metadata system powers the core decorators:

If you want to avoid metadata, consider: