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.
What is Metadata?
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:
- Store configuration on classes, methods, and parameters
- Build powerful decorator systems
- Implement dependency injection patterns
- Create validation and serialization frameworks
- Accumulate metadata from multiple decorators
This library provides a simplified, functional API over reflect-metadata that supports accumulation patterns through mapper functions.
Prerequisites
To use the metadata system, you need:
- Import
reflect-metadataat your application entry point - Enable
experimentalDecoratorsandemitDecoratorMetadatain yourtsconfig.json
reflect-metadata is an optional consumer dependency. The runtime package stays dependency-free, so applications using metadata decorators must install and initialize it themselves.
// 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
}
}API Overview
The metadata module exports functions organized in pairs per target type:
- Class Metadata:
classMeta,getClassMeta,classLabel,getClassLabels,classTag,getClassTags - Parameter Metadata:
paramMeta,getParamMeta,paramLabel,getParamLabels,paramTag,getParamTags - Method Metadata:
methodMeta,getMethodMeta,methodLabel,getMethodLabels,methodTag,getMethodTags
Class Metadata
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
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']@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
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
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']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')); // trueclass 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')); // trueMethod Metadata
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
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']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')); // falseclass 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')); // falseHow It Works
The metadata system is built on top of reflect-metadata:
- Class metadata uses
Reflect.defineMetadataandReflect.getOwnMetadataon the class constructor - Parameter metadata stores an array where each index corresponds to a parameter position
- Method metadata uses
Reflect.defineMetadatawith the class constructor and property key - All getter functions accept both a constructor and an instance — instances are resolved to their constructor automatically via
resolveConstructor
Usage in ts-ioc-container
The metadata system powers the core decorators:
- @register - Uses
classMetato store registration transformers - @inject - Uses
paramMetato specify constructor dependencies - @onConstruct/@onDispose/@hook - Use
methodMetafor lifecycle hooks
Best Practices
- Use unique metadata keys - Namespace your keys to avoid collisions (e.g.,
'mylib:feature') - Document metadata keys - Create constants for keys and document their purpose
- Use mapper functions for accumulation - When stacking decorators, use the
prevparameter to accumulate values - Type your metadata - Use TypeScript generics to type your metadata values
- Handle undefined gracefully - Always check if metadata exists before using it
- Prefer labels over raw metadata for key/value data -
classLabel/methodLabel/paramLabelprovide a typed, structured alternative to raw metadata keys - Prefer tags for boolean flags -
classTag/methodTag/paramTagavoid duplicates automatically viaSet - Don’t overuse metadata - Prefer explicit configuration over implicit metadata when possible
Limitations
- Requires reflect-metadata - Adds a runtime dependency and polyfill
- TypeScript experimental feature - Decorators are still experimental in TypeScript
- No static type checking - Metadata keys and values aren’t type-checked at compile time
- Reflection overhead - There’s a small performance cost to reading metadata
- Not serializable - Metadata doesn’t survive JSON serialization
Alternative Approaches
If you want to avoid metadata, consider:
- Explicit registration - Register dependencies manually without decorators
- Factory functions - Use factory functions instead of class decorators
- Configuration objects - Pass configuration explicitly rather than via metadata
- Type-based injection - Use TypeScript’s type system without reflection