Hone logo
Hone
Problems

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:

  1. Define a decorator factory: This factory will allow you to attach metadata to a target (class, method, or property).
  2. Store metadata: Implement a mechanism to store the metadata associated with each target.
  3. 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, attaches value under key to the decorated target.
  • Reflect.getMetadata(metadataKey: any, target: Object) function: This function should retrieve the metadata associated with metadataKey from the target object. If no metadata is found, it should return undefined.
  • 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 Map or a plain JavaScript object can work. Consider how to associate metadata with specific targets (classes, methods, properties).
  • The Reflect object 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.getMetadata signature often includes an optional propertyKey argument.
  • Think about the order of decorators. The @ReflectMetadata factory 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.
Loading editor...
typescript