Core Principles
Explicit Dependencies
Dependencies are declared up front with
.dependencies(), not discovered at runtimeType-Driven
TypeScript inference flows through tasks, resources, and middleware automatically
Composition Over Inheritance
Build complex behavior by composing simple functions and resources
Testable by Default
Call
.run() with mocks or run the full app—no special test harnessesBasic Dependency Injection
Declare what a task needs, and Runner provides it:Static vs. Dynamic Dependencies
Dependencies can be defined as a static object or as a function:- Conditional dependencies based on configuration
- Breaking circular type inference (see Handling Circular Dependencies)
- Late-binding scenarios where dependency choice depends on runtime config
Resource Dependencies
Resources can depend on other resources, creating a dependency graph:- Resolves the dependency graph topologically
- Initializes resources in the correct order
- Disposes resources in reverse order
- Detects circular dependencies and fails fast
Task Dependencies
Tasks can depend on resources, other tasks, events, and more:Middleware Dependencies
Middleware can also inject dependencies:Optional Dependencies
Mark dependencies as optional to support graceful degradation:Dependency Configuration
Resources can be configured when registered:Built-in Global Dependencies
Runner provides commonly-used resources out of the box:Testing with Dependency Injection
DI makes testing straightforward:Type Extraction
Extract dependency types for reuse:Dependency Graph Visualization
Runner Dev Tools provides visual dependency graphs:Best Practices
Keep Dependencies Minimal
Only inject what a task actually needs—don’t over-inject
Use Interfaces
Depend on interfaces/types, not concrete implementations
Avoid God Objects
Don’t create resources that do everything—keep them focused
Test with Mocks
Use
.run() with mocks for fast unit tests