Skip to content

Epic: World-Class TypeScript Patterns - Decorators, DI, and Advanced Type System #6292

@wtfsayo

Description

@wtfsayo

Overview

This epic captures opportunities to implement advanced TypeScript patterns including decorators, dependency injection, and type system enhancements that would make ElizaOS a world-class TypeScript codebase.

Analysis performed by: 6 parallel senior TypeScript engineer agents analyzing the entire monorepo


Executive Summary

Category Pattern Impact Effort Breaking Changes
Types Branded UUIDs 🟢 High Low Gradual migration
Core Service Decorators 🟢 High High Medium
Plugin @Action/@Provider Decorators 🟢 High Medium Low (opt-in)
Server Shared API Contracts 🟢 High Medium Low
CLI @command Decorators 🟡 Medium Medium Low
Database @transactional Decorator 🟡 Medium Low None
Core DI Container 🟡 Medium High High

1. Branded UUID Types (Priority: Critical)

Problem: All UUIDs are interchangeable - agentId, roomId, entityId can be accidentally swapped at runtime.

Solution:

// Branded types prevent mixing at compile-time
declare const __brand: unique symbol;
type Brand<T, B> = T & { readonly [__brand]: B };

type AgentId = Brand<UUID, 'AgentId'>;
type RoomId = Brand<UUID, 'RoomId'>;
type EntityId = Brand<UUID, 'EntityId'>;

// Compile-time error:
function getAgent(agentId: AgentId): Agent;
getAgent(someRoomId); // ❌ Error: RoomId not assignable to AgentId

Files: packages/core/src/types/primitives.ts


2. Component Decorators (@action, @Provider, @service)

Current - Manual plugin assembly:

export const bootstrapPlugin: Plugin = {
  name: 'bootstrap',
  actions: [action1, action2, action3, ...], // 15+ manual entries
  providers: [provider1, provider2, ...],
  services: [Service1, Service2],
};

Proposed - Auto-discovered via decorators:

@Action({ name: 'REPLY', similes: ['GREET', 'RESPOND'] })
export class ReplyAction implements IAction {
  @Inject() private runtime!: IAgentRuntime;
  
  async validate(): Promise<boolean> { return true; }
  
  async handler(message: Memory): Promise<ActionResult> {
    // Runtime auto-injected, cleaner signatures
  }
}

@Provider({ name: 'TIME', position: 1 })
export class TimeProvider implements IProvider {
  async get(): Promise<ProviderResult> { /* ... */ }
}

@Plugin({ name: 'bootstrap' })
export class BootstrapPlugin {} // Components auto-registered via decorator metadata

Files: Create packages/core/src/decorators/


3. Shared API Contracts Package

Problem: Server and client have duplicated/divergent type definitions

Solution:

// packages/api-contracts/src/index.ts - Single source of truth
import { z } from 'zod';

export const CreateAgentSchema = z.object({
  characterPath: z.string().optional(),
  characterJson: z.record(z.unknown()).optional(),
});
export type CreateAgentRequest = z.infer<typeof CreateAgentSchema>;

export const AgentSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  status: z.enum(['active', 'inactive']),
});
export type Agent = z.infer<typeof AgentSchema>;

// Route contracts for type-safe client
export const API_ROUTES = {
  agents: {
    list: { method: 'GET', path: '/api/agents', response: z.array(AgentSchema) },
    create: { method: 'POST', path: '/api/agents', body: CreateAgentSchema, response: AgentSchema },
  },
} as const;

Files: Create packages/api-contracts/


4. CLI Command Decorators

Current:

command('agent')
  .option('-j, --json', 'output as JSON')
  .option('-r, --remote-url <url>', 'URL of remote runtime')
  .action(handler);

Proposed:

@CommandModule({ name: 'agent', description: 'Manage ElizaOS agents' })
export class AgentCommand {
  constructor(@Inject(AgentsService) private agentsService: AgentsService) {}

  @SubCommand({ name: 'list', alias: 'ls' })
  async list(
    @Option({ flags: '-j, --json' }) json: boolean,
    @Option({ flags: '-r, --remote-url <url>' }) remoteUrl: string,
    @Option({ flags: '-p, --port <port>' }) @Validate(IsPort()) port: number,
  ): Promise<void> {
    const agents = await this.agentsService.listAgents();
    // ...
  }
}

Files: Create packages/cli/src/decorators/


5. Database Decorators

@transactional Decorator:

@Transactional({ isolation: 'serializable' })
async transferData(sourceId: AgentId, targetId: AgentId): Promise<void> {
  // All operations automatically wrapped in transaction with rollback on error
}

Repository Pattern:

@Repository({ entity: AgentEntity })
export class AgentRepository extends BaseRepository<Agent, AgentId> {
  // Automatic CRUD: findById, findAll, create, update, delete
  
  async findByName(name: string): Promise<Agent | null> {
    // Custom queries
  }
}

Files: Create packages/plugin-sql/src/decorators/


6. Type-Safe Event System

Current: String event names with manual typing
Proposed: Template literal types

type PlatformEventType = `${PlatformPrefix}:${EventType}`;
// Generates: 'DISCORD:MESSAGE_RECEIVED' | 'TELEGRAM:MESSAGE_RECEIVED' | ...

runtime.registerEvent('DISCORD:INVALID', handler); // ❌ Compile error
runtime.registerEvent('DISCORD:MESSAGE_RECEIVED', handler); // ✅ Type-safe

Files: packages/core/src/types/events.ts


7. Dependency Injection Container (Future)

@Injectable()
export class AgentService {
  constructor(
    @Inject(DI_TOKENS.RUNTIME) private runtime: IAgentRuntime,
    @Inject(DI_TOKENS.DATABASE) private db: IDatabaseAdapter,
  ) {}
}

// Container resolves dependencies automatically
const agentService = container.resolve(AgentService);

Recommendation: Consider for v2.0 due to architectural impact


Implementation Roadmap

Phase 1: Quick Wins (1-2 weeks)

  • Branded UUID types in primitives.ts
  • Type predicates file (types/guards.ts)
  • Eliminate any types in elizaos.ts
  • Template literal event types

Phase 2: Decorator Foundation (2-4 weeks)

Phase 3: CLI & Server Enhancement (2-3 weeks)

  • @CommandModule decorators for CLI
  • Controller decorators for server
  • Zod validation middleware
  • OpenAPI documentation decorators

Phase 4: Advanced Patterns (4-6 weeks)

  • Full DI container implementation
  • Module system with imports/exports
  • CQRS pattern for complex operations

Files to Create

File Purpose
packages/core/src/types/guards.ts Type predicates and guards
packages/core/src/decorators/index.ts Decorator exports
packages/core/src/decorators/action.ts @action decorator
packages/core/src/decorators/provider.ts @Provider decorator
packages/core/src/decorators/service.ts @service decorator
packages/core/src/decorators/inject.ts @Inject decorator
packages/core/src/di/container.ts DI container
packages/api-contracts/src/index.ts Shared API schemas
packages/cli/src/decorators/command.ts @command decorator
packages/plugin-sql/src/decorators/transactional.ts @transactional

Files to Modify

File Changes
packages/core/src/types/primitives.ts Add branded UUID types
packages/core/src/types/events.ts Template literal types
packages/core/src/types/database.ts Discriminated unions for logs
packages/core/src/types/elizaos.ts Replace any with specific types
packages/core/src/types/service.ts Enhanced ServiceClassMap
packages/core/src/runtime.ts Integrate decorator registry
packages/plugin-bootstrap/src/index.ts Optional module pattern
packages/server/src/middleware/validation.ts Zod integration

Benefits

Benefit Description
Type Safety Compile-time prevention of ID mixing, event typos, service mismatches
Reduced Boilerplate 40-60% less manual wiring code in plugins
Better DX IDE autocomplete, inline documentation, better error messages
Testability DI enables easy mocking and isolation
Maintainability Self-documenting decorators, single source of truth
Scalability Module boundaries, lazy loading, tree-shaking

References


🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions