Implementing a Macro System with Typescript
Macros are powerful tools that allow you to define reusable code snippets, often with placeholders for dynamic values. This challenge asks you to design and implement a type-safe macro system in Typescript, enabling you to create and apply macros with predictable type checking. This is useful for reducing code duplication, improving readability, and enforcing consistency across your codebase.
Problem Description
You need to create a system that allows defining macros and applying them to values. The system should be type-safe, meaning that the types of the macro's arguments and the resulting value should be correctly inferred and checked at compile time. The macro system should support:
- Macro Definition: Defining a macro with a name, a template string containing placeholders, and a type definition for the arguments passed to the placeholders.
- Macro Application: Applying a defined macro to a set of values, where each value corresponds to a placeholder in the macro's template.
- Type Safety: Ensuring that the types of the provided values match the expected types defined in the macro. The resulting value from applying the macro should also have a predictable type.
The core of the system revolves around a Macro type and a applyMacro function. The Macro type should encapsulate the macro's name, template, and argument types. The applyMacro function should take a Macro and a set of values as input and return a type-safe result.
Examples
Example 1:
Input:
macro: {
name: "logMessage",
template: "console.log('Message: {message}');",
argumentTypes: { message: string }
}
values: { message: "Hello, world!" }
Output:
void
Explanation:
The macro "logMessage" is applied with the value "Hello, world!" for the "message" placeholder. The resulting action is equivalent to `console.log('Message: Hello, world!');`. Since `console.log` returns `void`, the overall result is `void`.
Example 2:
Input:
macro: {
name: "formatName",
template: "{firstName} {lastName}",
argumentTypes: { firstName: string, lastName: string }
}
values: { firstName: "John", lastName: "Doe" }
Output:
"John Doe"
Explanation:
The macro "formatName" is applied with "John" for `firstName` and "Doe" for `lastName`. The template string is evaluated, resulting in the string "John Doe".
Example 3: (Edge Case - Type Mismatch)
Input:
macro: {
name: "add",
template: "{a} + {b}",
argumentTypes: { a: number, b: number }
}
values: { a: 10, b: "20" }
Output:
Type Error: Argument type mismatch for 'b'. Expected number, got string.
Explanation:
The macro "add" expects both 'a' and 'b' to be numbers. However, 'b' is provided as a string. The system should detect this type mismatch and throw a type error (or return an appropriate error value). The exact error handling mechanism is up to you, but it *must* be detectable at compile time or runtime.
Constraints
- The macro template string should support simple string interpolation using curly braces
{}as placeholders. - The
applyMacrofunction should perform type checking before applying the macro. - The system should handle type mismatches gracefully, providing informative error messages.
- The macro system should be extensible to support more complex template string operations (e.g., function calls within the template) in the future. While not required for this challenge, consider this when designing your types.
- Assume that the values provided to
applyMacroare always an object.
Notes
- Consider using generics to ensure type safety.
- Think about how to represent the macro's argument types effectively. A simple object mapping placeholder names to types might be sufficient.
- You don't need to implement a full-fledged template engine. Focus on the type-safe application of the macro.
- Error handling can be done through exceptions or by returning a specific error value. Choose the approach that best suits your design.
- The goal is to demonstrate a type-safe macro system, not to create a production-ready macro engine. Keep the scope focused on the core requirements.