Hone logo
Hone
Problems

Extensible Effects System in TypeScript

This challenge asks you to design and implement a simple, extensible effects system in TypeScript. Effects are side-effecting operations (like logging, database access, or network requests) that can be composed and managed in a predictable way. Building such a system promotes modularity, testability, and separation of concerns in your code.

Problem Description

You need to create a system that allows you to define and execute "effects." These effects should be composable, meaning you can chain them together to perform a sequence of actions. The system should be extensible, allowing new effects to be added without modifying the core effect execution logic.

What needs to be achieved:

  1. Effect Definition: Define a type that represents an effect. This type should be generic, allowing it to represent different kinds of effects.
  2. Effect Execution: Create a function that takes an array of effects and executes them sequentially.
  3. Extensibility: Design the system so that new effects can be added by simply creating new objects that conform to the effect type, without needing to change the core execution function.
  4. Context (Optional but Recommended): Consider how to pass a context object through the effects. This context could contain data needed by multiple effects.

Key Requirements:

  • The system must be type-safe. TypeScript should enforce that effects are used correctly.
  • The system must be extensible. Adding new effects should be straightforward.
  • The system should handle errors gracefully. If an effect throws an error, the execution should stop (or potentially continue, depending on your design choice - document your choice).
  • The system should be composable. Effects should be able to be chained together.

Expected Behavior:

When an array of effects is passed to the execution function, each effect should be executed in order. The results of each effect (if any) should be handled appropriately (e.g., logged, stored, or passed to the next effect).

Edge Cases to Consider:

  • Empty array of effects: Should do nothing.
  • Effects that throw errors: How should these be handled? Should execution stop, or should the error be caught and handled?
  • Effects that return values: How should these values be handled? Should they be ignored, or should they be passed to the next effect?
  • Context: How does the context object get passed to each effect?

Examples

Example 1:

Input: [ { type: 'log', message: 'Starting process' }, { type: 'delay', milliseconds: 1000 }, { type: 'log', message: 'Process complete' } ]
Output: (Console logs)
"Starting process"
(1 second delay)
"Process complete"
Explanation: The effects are executed sequentially. The 'log' effects print messages to the console, and the 'delay' effect pauses execution for 1 second.

Example 2:

Input: [ { type: 'fetchData', url: 'https://example.com/data' }, { type: 'processData', data: (data from fetchData) } ]
Output: (Assuming fetchData succeeds and processData logs the processed data)
(Network request to example.com/data)
"Processed data: { ... }"
Explanation: This demonstrates chaining effects where one effect's result is used by the next.  The `fetchData` effect retrieves data, and the `processData` effect processes it.

Example 3: (Error Handling)

Input: [ { type: 'log', message: 'Starting' }, { type: 'throwError', message: 'Simulated error' }, { type: 'log', message: 'This will not be executed' } ]
Output: (Console log and error)
"Starting"
Error: Simulated error
Explanation: The `throwError` effect throws an error, which causes the execution to stop. The final 'log' effect is not executed.

Constraints

  • The solution must be written in TypeScript.
  • The core execution function should be as generic as possible to accommodate different effect types.
  • The solution should be reasonably performant. Avoid unnecessary overhead.
  • The solution should be well-documented and easy to understand.
  • The solution should handle at least one error case gracefully.

Notes

  • Consider using discriminated unions to define the different types of effects. This will allow TypeScript to provide better type checking.
  • Think about how to handle asynchronous effects. You might need to use async/await or Promises.
  • The context object is optional, but it's a good practice to include it to make your effects more reusable.
  • Focus on the core principles of extensibility and composability. Don't overcomplicate the system with unnecessary features.
  • Document your design choices and any assumptions you make.
Loading editor...
typescript