Implement Reflect Metadata Types in TypeScript
This challenge focuses on implementing a simplified version of the reflect-metadata API in TypeScript. You will create decorators that allow you to associate arbitrary metadata with classes, methods, and properties. This technique is fundamental for frameworks like Angular and NestJS, enabling powerful features like dependency injection and configuration.
Problem Description
Your task is to create a set of TypeScript decorators that mimic the core functionality of the reflect-metadata library. Specifically, you need to:
- Define a decorator factory: This factory will allow you to attach metadata to a target (class, method, or property).
- Store metadata: Implement a mechanism to store the metadata associated with each target.
- Retrieve metadata: Implement a function to retrieve the metadata associated with a specific target and metadata key.
Key Requirements:
@ReflectMetadata(key: any, value: any)decorator factory: This factory should return a decorator that, when applied, attachesvalueunderkeyto the decorated target.Reflect.getMetadata(metadataKey: any, target: Object)function: This function should retrieve the metadata associated withmetadataKeyfrom thetargetobject. If no metadata is found, it should returnundefined.- Support for classes, methods, and properties: The decorators should be applicable to all these types.
Expected Behavior:
When a decorator like @ReflectMetadata('myKey', 'myValue') is applied to a class, method, or property, the value 'myValue' should be associated with the key 'myKey' for that specific target. Subsequent calls to Reflect.getMetadata('myKey', target) should return 'myValue'.
Edge Cases:
- What happens if metadata is defined multiple times for the same key on the same target? The most recently defined value should take precedence.
- What happens if you try to retrieve metadata for a key that was never defined for a target? It should return
undefined. - Consider how to handle metadata for different types of targets (classes vs. instances). For this challenge, focus on attaching metadata directly to the constructor function for classes, and to the prototype for methods. For properties, associate metadata with the property key on the prototype.
Examples
Example 1: Class Metadata
// Assume ReflectMetadata and Reflect.getMetadata are implemented
@ReflectMetadata('design:type', String)
@ReflectMetadata('version', 1.0)
class User {
name: string;
}
// Retrieve metadata from the class constructor
const typeMetadata = Reflect.getMetadata('design:type', User);
const versionMetadata = Reflect.getMetadata('version', User);
const nonExistentMetadata = Reflect.getMetadata('age', User);
console.log(typeMetadata); // Expected: String
console.log(versionMetadata); // Expected: 1.0
console.log(nonExistentMetadata); // Expected: undefined
Explanation:
The @ReflectMetadata decorator factory is used to attach metadata to the User class. Reflect.getMetadata is then used to retrieve these values using their respective keys.
Example 2: Method Metadata
// Assume ReflectMetadata and Reflect.getMetadata are implemented
class Product {
@ReflectMetadata('http:method', 'GET')
@ReflectMetadata('auth:required', true)
getData() {
// ... method implementation
}
}
// Retrieve metadata from the method on the prototype
const methodProto = Product.prototype;
const httpMethodMetadata = Reflect.getMetadata('http:method', methodProto, 'getData');
const authRequiredMetadata = Reflect.getMetadata('auth:required', methodProto, 'getData');
console.log(httpMethodMetadata); // Expected: 'GET'
console.log(authRequiredMetadata); // Expected: true
Explanation:
Metadata is attached to the getData method. Note that to get method metadata, you typically need to provide the method name as a third argument to Reflect.getMetadata.
Example 3: Property Metadata
// Assume ReflectMetadata and Reflect.getMetadata are implemented
class Config {
@ReflectMetadata('config:type', 'database')
dbConnection: string;
@ReflectMetadata('config:default', 'localhost')
host: string;
}
// Retrieve metadata from the property on the prototype
const configProto = Config.prototype;
const dbTypeMetadata = Reflect.getMetadata('config:type', configProto, 'dbConnection');
const hostDefaultMetadata = Reflect.getMetadata('config:default', configProto, 'host');
console.log(dbTypeMetadata); // Expected: 'database'
console.log(hostDefaultMetadata); // Expected: 'localhost'
Explanation:
Metadata is attached to the dbConnection and host properties. Similar to methods, you provide the property name to Reflect.getMetadata.
Constraints
- Your implementation must be purely in TypeScript.
- You should not use any external libraries like
reflect-metadata. You are implementing it. - The metadata storage mechanism should be efficient for typical use cases.
Notes
- You'll need to use a suitable data structure to store the metadata. A
Mapor a plain JavaScript object can work. Consider how to associate metadata with specific targets (classes, methods, properties). - The
Reflectobject is a global object available in JavaScript. You will be augmenting or creating functions within this object. - For methods and properties, you'll need to consider how to associate metadata with a specific member name on a given prototype. The
Reflect.getMetadatasignature often includes an optionalpropertyKeyargument. - Think about the order of decorators. The
@ReflectMetadatafactory should ensure that metadata from outer decorators is applied after inner decorators, so the outermost decorator's metadata takes precedence in case of conflicts if you were to chain them in the standard way. However, for this challenge, focus on making sure the last applied decorator for a given key/target pair overwrites previous ones.