TypeScript Metaprogramming: Building a Decorator Factory
Metaprogramming in TypeScript allows you to write code that manipulates or generates other code. This challenge focuses on creating a flexible system for defining and applying decorators, a powerful metaprogramming feature in TypeScript. You will build a foundation for a decorator factory that allows for parameterized decorators and can be extended for more complex scenarios.
Problem Description
Your task is to create a TypeScript type system that allows for the creation of decorator factories. A decorator factory is a function that returns a decorator. This pattern is essential when you want to pass arguments to your decorators. For instance, you might want a @log decorator that can optionally take a prefix string.
You need to define the core types and a helper function to facilitate this. The primary goal is to achieve type safety when defining and applying these parameterized decorators to classes and class members.
Key Requirements:
- Decorator Factory Type: Define a type that accurately represents a function which accepts arguments and returns a decorator function.
- Decorator Type: Define a type that accurately represents a TypeScript decorator (for classes, methods, properties, etc.).
createDecoratorFactoryHelper: Implement a utility function namedcreateDecoratorFactory. This function should:- Accept the factory function (the one that returns the decorator) as its argument.
- Return a new function that, when called with the factory's arguments, returns the actual decorator.
- Crucially, it should correctly infer and preserve the types of the arguments passed to the factory.
- Type Safety: Ensure that when using the
createDecoratorFactory, TypeScript can infer the argument types for the factory and correctly type the resulting decorator.
Expected Behavior:
When a decorator factory is created using your createDecoratorFactory and then invoked with arguments, the returned decorator should behave as expected when applied to a class or class member. The type system should prevent type errors if incorrect arguments are passed to the factory or if the decorator is applied to an incompatible target.
Edge Cases:
- Decorators without arguments (factories that don't accept parameters).
- Decorators applied to different targets (classes, methods, properties, parameters). While the core challenge is about the factory, consider how the types would naturally extend.
Examples
Example 1: Simple Parameterized Class Decorator
Let's say we want a @log decorator factory that accepts a prefix string.
// Assume createDecoratorFactory and the necessary decorator types are defined
// Define the arguments for our decorator factory
interface LogDecoratorArgs {
prefix: string;
}
// The actual decorator function logic
function logDecorator(args: LogDecoratorArgs) {
return function <T>(target: new (...args: any[]) => T) {
console.log(`${args.prefix}: Decorating class ${target.name}`);
// In a real scenario, you might add functionality here
};
}
// Create the decorator factory using our helper
const createLogDecorator = createDecoratorFactory(logDecorator);
// Apply the parameterized decorator
@createLogDecorator({ prefix: "INFO" })
class MyService {
// ...
}
// Expected Output in Console:
// INFO: Decorating class MyService
Explanation:
createLogDecorator is a function that, when called with an object { prefix: "INFO" }, returns the logDecorator configured with that prefix. This returned decorator is then applied to MyService.
Example 2: Decorator Factory with Multiple Arguments
A factory for a @auth decorator that takes role and permission.
// Assume createDecoratorFactory and the necessary decorator types are defined
interface AuthDecoratorArgs {
role: 'admin' | 'user';
permission: 'read' | 'write';
}
function authDecorator(args: AuthDecoratorArgs) {
return function <T>(
target: Object, // For method decorators, target is prototype
propertyKey: string | symbol, // The name of the method
descriptor: PropertyDescriptor // The property descriptor
) {
console.log(`Auth check: Role=${args.role}, Permission=${args.permission} for method ${String(propertyKey)}`);
// In a real scenario, you'd add authorization logic
};
}
const createAuthDecorator = createDecoratorFactory(authDecorator);
class UserController {
@createAuthDecorator({ role: 'admin', permission: 'write' })
createUser(data: any) {
console.log("Creating user...");
}
@createAuthDecorator({ role: 'user', permission: 'read' })
getProfile() {
console.log("Fetching profile...");
}
}
// When UserController is defined, the console logs will appear.
// If you were to then call UserController.prototype.createUser.call(instance),
// the authorization logic would be executed.
// Expected Output in Console (when class is defined):
// Auth check: Role=admin, Permission=write for method createUser
// Auth check: Role=user, Permission=read for method getProfile
Explanation:
createAuthDecorator takes the authDecorator function. When createAuthDecorator({ role: 'admin', permission: 'write' }) is called, it returns the actual decorator, which is then applied to the createUser method.
Constraints
- The solution must be written entirely in TypeScript.
- The
createDecoratorFactoryfunction should have a generic signature that allows for the inference of the decorator factory's argument types. - The solution should not rely on any external libraries.
- The types for class, method, property, and parameter decorators should be correctly represented.
Notes
- Consider the different signatures for TypeScript decorators: class decorators, property decorators, method decorators, and parameter decorators. You'll need to account for these in your type definitions.
- Think about how you can use generics to make
createDecoratorFactoryas reusable and type-safe as possible. - The core of this challenge is defining the types and the
createDecoratorFactoryhelper function. The actual decorator logic within the examples is illustrative. - You might find it useful to look up the standard TypeScript decorator type signatures.