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:
- Defining Metadata Keys: A consistent way to identify different types of metadata.
- Decorating Classes and Properties: Using decorators to attach metadata.
- 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
getMetadatafunction should return the stored metadata for a given key and target (class or property key). - If no metadata is found for a given key,
getMetadatashould returnundefined.
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
getMetadatafunction 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.jsonhas"experimentalDecorators": trueand"emitDecoratorMetadata": trueenabled. - Consider how to store metadata. A
Mapor 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
targetin the decorator will be the class prototype. - When defining your
getMetadatafunction, consider how to handle thetargetargument for both class and property reflections. - You can define your own string-based metadata keys. For a more advanced system, you might explore
Symbolor unique object instances as keys.