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
// 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 six functions organized in pairs:
- Class Metadata:
setClassMetadataandgetClassMetadata - Parameter Metadata:
setParameterMetadataandgetParameterMetadata - Method Metadata:
setMethodMetadataandgetMethodMetadata
Class Metadata
Class metadata is stored on the class constructor itself. Use this to attach configuration, tags, or registration information to classes.
setClassMetadata
Creates a class decorator that stores metadata using a mapper function. The mapper receives the previous value (if any) and returns the new value.
setClassMetadata<T>(
key: string | symbol,
mapFn: (prev: T | undefined) => T
): ClassDecorator
getClassMetadata
Retrieves metadata from a class constructor.
getClassMetadata<T>(
target: object,
key: string | symbol
): T | undefined
Example
const TAGS_KEY = 'tags';
@setClassMetadata(TAGS_KEY, (prev: string[] = []) => [...prev, 'service'])
@setClassMetadata(TAGS_KEY, (prev: string[] = []) => [...prev, 'api'])
class ApiService {}
const tags = getClassMetadata<string[]>(ApiService, TAGS_KEY);
console.log(tags); // ['api', 'service']const TAGS_KEY = 'tags';
@setClassMetadata(TAGS_KEY, (prev: string[] = []) => [...prev, 'service'])
@setClassMetadata(TAGS_KEY, (prev: string[] = []) => [...prev, 'api'])
class ApiService {}
const tags = getClassMetadata<string[]>(ApiService, TAGS_KEY);
console.log(tags); // ['api', 'service']Parameter Metadata
Parameter metadata is stored as an array indexed by parameter position. This is crucial for constructor and method parameter injection.
setParameterMetadata
Creates a parameter decorator that stores metadata for a specific parameter. The mapper receives the previous value for that parameter index.
setParameterMetadata(
key: string | symbol,
mapFn: (prev: unknown) => unknown
): ParameterDecorator
getParameterMetadata
Retrieves parameter metadata as an array. Undecorated parameters will have undefined at their index.
getParameterMetadata(
key: string | symbol,
target: constructor<unknown>
): unknown[]
Example
const INJECT_KEY = 'inject:constructor';
class DatabaseService {
constructor(
@setParameterMetadata(INJECT_KEY, () => 'config') config: any,
@setParameterMetadata(INJECT_KEY, () => 'logger') logger: any,
) {}
}
const metadata = getParameterMetadata(INJECT_KEY, DatabaseService);
console.log(metadata); // ['config', 'logger']const INJECT_KEY = 'inject:constructor';
class DatabaseService {
constructor(
@setParameterMetadata(INJECT_KEY, () => 'config') config: any,
@setParameterMetadata(INJECT_KEY, () => 'logger') logger: any,
) {}
}
const metadata = getParameterMetadata(INJECT_KEY, DatabaseService);
console.log(metadata); // ['config', 'logger']Method Metadata
Method metadata is stored per method on the class. Use this for hooks, validators, middleware, or any method-level configuration.
setMethodMetadata
Creates a method decorator that stores metadata for a specific method. The mapper receives the previous value for that method.
setMethodMetadata<T>(
key: string,
mapFn: (prev: T | undefined) => T
): MethodDecorator
getMethodMetadata
Retrieves metadata for a specific method.
getMethodMetadata(
key: string,
target: object,
propertyKey: string
): unknown
Example
const MIDDLEWARE_KEY = 'middleware';
class Controller {
@setMethodMetadata(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'auth'])
@setMethodMetadata(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'validate'])
handleRequest() {}
}
const controller = new Controller();
const middleware = getMethodMetadata(MIDDLEWARE_KEY, controller, 'handleRequest');
console.log(middleware); // ['validate', 'auth']const MIDDLEWARE_KEY = 'middleware';
class Controller {
@setMethodMetadata(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'auth'])
@setMethodMetadata(MIDDLEWARE_KEY, (prev: string[] = []) => [...prev, 'validate'])
handleRequest() {}
}
const controller = new Controller();
const middleware = getMethodMetadata(MIDDLEWARE_KEY, controller, 'handleRequest');
console.log(middleware); // ['validate', 'auth']How 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
Usage in ts-ioc-container
The metadata system powers the core decorators:
- @register - Uses
setClassMetadatato store registration transformers - @inject - Uses
setParameterMetadatato specify constructor dependencies - @onConstruct/@onDispose/@hook - Use
setMethodMetadatafor 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
- 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