Advanced Typescript Metaprogramming: Dynamic Plugin System
This challenge focuses on building a foundational metaprogramming framework in Typescript to create a dynamic plugin system. The goal is to define types that allow you to register plugins with specific configurations at runtime, ensuring type safety while maintaining flexibility. This is useful for building extensible applications where functionality can be added or modified without recompilation.
Problem Description
You need to create a Typescript framework that allows you to define and register plugins. Each plugin should have a unique identifier, a configuration object of a specific type, and a function that performs some action. The framework should ensure that the configuration provided to a plugin matches the plugin's expected configuration type.
Key Requirements:
-
Plugin<TConfig>Type: Define a generic typePlugin<TConfig>that represents a plugin. It should have the following properties:id: A string representing the plugin's unique identifier.config: A type aliasTConfigrepresenting the expected configuration for the plugin.execute: A function that takes the configurationTConfigas input and returns a value of any type (unknown).
-
PluginRegistryType: Define a typePluginRegistrywhich is a record (object) where keys are plugin IDs (strings) and values arePlugin<TConfig>instances. -
registerPlugin<TConfig>(plugin: Plugin<TConfig>, id: string): PluginRegistryFunction: Create a functionregisterPluginthat takes aPlugin<TConfig>and its ID as input and adds it to aPluginRegistry. The function should return the updatedPluginRegistry. The function should throw an error if a plugin with the same ID already exists in the registry. -
getPlugin<TConfig>(registry: PluginRegistry, id: string): Plugin<TConfig> | undefinedFunction: Create a functiongetPluginthat takes aPluginRegistryand a plugin ID as input and returns the plugin associated with that ID, orundefinedif no such plugin exists. -
Configuration Type Enforcement: The
registerPluginfunction must enforce that the configuration provided when executing a plugin matches the plugin's declared configuration type.
Expected Behavior:
- Plugins should be registered with unique IDs.
- The
executefunction of a plugin should be called with the correct configuration type. - Attempting to register a plugin with a duplicate ID should result in an error.
- Retrieving a non-existent plugin should return
undefined.
Edge Cases to Consider:
- Empty plugin registry.
- Plugin with an empty configuration type (
TConfigisnever). - Plugin ID is an empty string.
- Plugin ID is null or undefined (should be handled gracefully, likely by throwing an error).
Examples
Example 1:
interface MyConfig {
name: string;
age: number;
}
const plugin1: Plugin<MyConfig> = {
id: 'myPlugin',
config: MyConfig,
execute: (config) => `Hello, ${config.name}! You are ${config.age} years old.`
};
const registry: PluginRegistry = {};
const updatedRegistry = registerPlugin(plugin1, 'myPlugin');
const retrievedPlugin = getPlugin(updatedRegistry, 'myPlugin');
// Expected Output:
// retrievedPlugin?.execute({ name: "Alice", age: 30 }) === "Hello, Alice! You are 30 years old."
Example 2:
interface AnotherConfig {
enabled: boolean;
}
const plugin2: Plugin<AnotherConfig> = {
id: 'anotherPlugin',
config: AnotherConfig,
execute: (config) => `Plugin enabled: ${config.enabled}`
};
const registry2: PluginRegistry = {
'myPlugin': plugin1
};
const updatedRegistry2 = registerPlugin(plugin2, 'anotherPlugin');
const retrievedPlugin2 = getPlugin(updatedRegistry2, 'anotherPlugin');
// Expected Output:
// retrievedPlugin2?.execute({ enabled: true }) === "Plugin enabled: true"
Example 3: (Error Handling)
interface Config1 {
setting1: string;
}
const plugin3: Plugin<Config1> = {
id: 'duplicatePlugin',
config: Config1,
execute: (config) => `Executing with ${config.setting1}`
};
const registry3: PluginRegistry = {
'myPlugin': plugin1
};
try {
const updatedRegistry3 = registerPlugin(plugin3, 'myPlugin'); // Should throw an error
} catch (error) {
// Expected Output: Error message indicating duplicate plugin ID
console.error(error.message);
}
Constraints
- All code must be written in Typescript.
- The solution should be well-structured and readable.
- The
PluginRegistryshould be a standard Javascript object (record). - Error handling should be robust and informative.
- The solution should be reasonably performant (avoid unnecessary iterations or complex operations).
Notes
- Consider using conditional types and mapped types to make the framework more generic and reusable.
- Think about how to handle potential errors during plugin execution (e.g., invalid configuration values).
- This is a foundational framework; you can extend it with features like plugin loading, dependency injection, and lifecycle management.
- Focus on type safety and ensuring that the configuration provided to each plugin matches its expected type. This is the core of the challenge.