Hone logo
Hone
Problems

TypeScript Metadata Reflection Types

This challenge focuses on creating a system for defining and accessing metadata associated with classes and their properties in TypeScript. This is a common requirement for frameworks and libraries that need to inspect and manipulate code at runtime, such as for dependency injection, serialization, or validation. You will build a foundation for a metadata reflection system.

Problem Description

The goal is to design and implement a TypeScript system that allows developers to attach arbitrary metadata to classes and their properties. This metadata should be retrievable at runtime. You need to create mechanisms for:

  1. Defining Metadata Keys: A consistent way to identify different types of metadata.
  2. Decorating Classes and Properties: Using decorators to attach metadata.
  3. Reflecting Metadata: A function to retrieve metadata associated with a class or its properties.

Key Requirements:

  • Metadata Storage: Metadata should be stored in a way that it can be associated with specific classes and properties.
  • Key-Value Pairs: Metadata should be stored as key-value pairs, where keys are unique identifiers and values can be of any type.
  • Class Metadata: Ability to attach metadata to a class itself.
  • Property Metadata: Ability to attach metadata to specific properties of a class.
  • Reflection Function: A universal function getMetadata(metadataKey, target) that can retrieve metadata for both classes and properties.
  • Type Safety: Leverage TypeScript's type system to provide as much type safety as possible, especially for metadata keys.

Expected Behavior:

  • When a decorator attaches metadata, it should be stored.
  • The getMetadata function should return the stored metadata for a given key and target (class or property key).
  • If no metadata is found for a given key, getMetadata should return undefined.

Edge Cases to Consider:

  • Accessing metadata for a property that doesn't exist.
  • Accessing metadata for a class that hasn't had any metadata attached.
  • Handling different types of metadata values (primitives, objects, functions).

Examples

Example 1: Class Metadata

Let's assume we have a custom decorator @ClassMetadata('version', '1.0.0') and @ClassMetadata('author', 'Jane Doe').

// Hypothetical decorator definition (you will implement this)
function ClassMetadata<T>(key: string, value: T) {
    return function (target: any) {
        // Implementation details...
    };
}

// Hypothetical reflection function (you will implement this)
declare function getMetadata<T>(metadataKey: string, target: any): T | undefined;

@ClassMetadata('version', '1.0.0')
@ClassMetadata('author', 'Jane Doe')
class MyClass {}

// Usage of reflection
const version = getMetadata<string>('version', MyClass);
const author = getMetadata<string>('author', MyClass);
const nonExistent = getMetadata<number>('someKey', MyClass);

console.log(version); // Expected: '1.0.0'
console.log(author);  // Expected: 'Jane Doe'
console.log(nonExistent); // Expected: undefined

Example 2: Property Metadata

Let's assume we have a custom decorator @PropertyMetadata('serialize', true) for a property.

// Hypothetical decorator definition (you will implement this)
function PropertyMetadata<T>(key: string, value: T) {
    return function (target: any, propertyKey: string | symbol) {
        // Implementation details...
    };
}

// Hypothetical reflection function (you will implement this)
declare function getMetadata<T>(metadataKey: string, target: any, propertyKey?: string | symbol): T | undefined;

class User {
    @PropertyMetadata('serialize', true)
    name: string;

    @PropertyMetadata('serialize', false)
    passwordHash: string;

    constructor(name: string, passwordHash: string) {
        this.name = name;
        this.passwordHash = passwordHash;
    }
}

// Usage of reflection
const serializeName = getMetadata<boolean>('serialize', User.prototype, 'name');
const serializePassword = getMetadata<boolean>('serialize', User.prototype, 'passwordHash');
const nonExistentPropMeta = getMetadata<string>('validation', User.prototype, 'name');

console.log(serializeName);     // Expected: true
console.log(serializePassword); // Expected: false
console.log(nonExistentPropMeta); // Expected: undefined

Example 3: Combined and Edge Cases

Consider a class with both class and property metadata, and a query for metadata that doesn't exist.

// Using the decorators and getMetadata from previous examples

@ClassMetadata('singleton', true)
class ConfigManager {}

class Product {
    @PropertyMetadata('databaseField', 'product_name')
    name: string;

    constructor(name: string) {
        this.name = name;
    }
}

// Usage of reflection
const isSingleton = getMetadata<boolean>('singleton', ConfigManager);
const dbField = getMetadata<string>('databaseField', Product.prototype, 'name');
const nonExistentClassMeta = getMetadata<string>('version', Product); // Class metadata for Product
const nonExistentPropMeta = getMetadata<boolean>('required', Product.prototype, 'name'); // Property metadata for name

console.log(isSingleton);       // Expected: true
console.log(dbField);           // Expected: 'product_name'
console.log(nonExistentClassMeta); // Expected: undefined
console.log(nonExistentPropMeta);  // Expected: undefined

Constraints

  • The solution must be implemented in TypeScript.
  • The metadata storage mechanism should be efficient for a moderate number of classes and properties (e.g., up to 1000 classes, each with up to 50 properties).
  • Decorators must be used to attach metadata.
  • A single getMetadata function should be responsible for retrieving all metadata.
  • Metadata keys should ideally be strings for simplicity in this challenge, but consider how to make them more robust.

Notes

  • You will need to use TypeScript decorators. Ensure your tsconfig.json has "experimentalDecorators": true and "emitDecoratorMetadata": true enabled.
  • Consider how to store metadata. A Map or a plain object keyed by target and property key might be suitable.
  • Think about how to differentiate between class metadata and property metadata within your storage mechanism.
  • For property metadata, the target in the decorator will be the class prototype.
  • When defining your getMetadata function, consider how to handle the target argument for both class and property reflections.
  • You can define your own string-based metadata keys. For a more advanced system, you might explore Symbol or unique object instances as keys.
Loading editor...
typescript