Docs/Dependency injectionv1.0.0-rc.1

DI Decorators — API Reference

@tagna/udiot v1.0.0-rc.1


All DI decorators require emitDecoratorMetadata: true and experimentalDecorators: true in tsconfig.json, and import 'reflect-metadata' at your application entry point.


@Injectable(options?)

Target: class

Marks a class as injectable. The decorator reads TypeScript's emitted constructor parameter types and stores them as InjectableMetadata so the container can resolve dependencies automatically.

function Injectable(options?: InjectableOptions): ClassDecorator

InjectableOptions

interface InjectableOptions {
  scope?: ServiceScope;      // Default: ServiceScope.SINGLETON
  providedIn?: string | 'root';
}
Option Description
scope Lifecycle of instances — SINGLETON, TRANSIENT, or SCOPED.
providedIn When set to 'root', the class is automatically registered in globalContainer on first resolve().
import { Injectable, ServiceScope } from '@tagna/udiot';

@Injectable()
class UserService {
  constructor(private db: DatabaseService) {}
}

@Injectable({ scope: ServiceScope.TRANSIENT })
class RequestLogger {
  log(msg: string): void { console.log(msg); }
}

@Injectable({ scope: ServiceScope.SINGLETON, providedIn: 'root' })
class ConfigService {
  readonly apiUrl = process.env.API_URL ?? 'http://localhost:3000';
}

@Inject(token)

Target: constructor parameter

Overrides the automatically inferred token for a single constructor argument. Required when injecting string tokens, Symbol tokens, or when emitDecoratorMetadata cannot infer the type correctly (e.g., interfaces, which are erased at runtime).

function Inject(token: InjectionToken): ParameterDecorator
import { Injectable, Inject } from '@tagna/udiot';

const API_KEY = Symbol('ApiKey');

@Injectable()
class ApiClient {
  constructor(
    @Inject('API_URL') private apiUrl: string,
    @Inject(API_KEY)   private apiKey: string,
    private logger: LoggerService  // type-based, no @Inject needed
  ) {}
}

container.registerValue('API_URL', 'https://api.example.com');
container.registerValue(API_KEY, process.env.API_KEY!);

@InjectProperty(token?)

Target: class property

Marks a property for injection after instance construction. The container resolves the property's type (via design:type metadata) if no token is supplied, or uses the explicit token if provided.

function InjectProperty(token?: InjectionToken): PropertyDecorator
import { Injectable, InjectProperty } from '@tagna/udiot';

@Injectable()
class OrderService {
  @InjectProperty()
  private userService: UserService;         // resolved by type

  @InjectProperty('CURRENCY_CODE')
  private currencyCode: string;             // resolved by string token

  processOrder(id: string): void {
    const user = this.userService.findById(id);
    // ...
  }
}

Property injection is useful for breaking circular dependencies, since the property is populated after the constructor runs.


@Optional()

Target: constructor parameter

Allows a dependency to be absent. If the token is not registered, undefined is injected instead of throwing ProviderNotFoundError.

function Optional(): ParameterDecorator
import { Injectable, Inject, Optional } from '@tagna/udiot';

@Injectable()
class AuditService {
  constructor(
    @Optional() @Inject('AUDIT_LOGGER') private logger?: AuditLogger
  ) {}

  record(event: string): void {
    this.logger?.write(event);
  }
}

@Optional() must appear before @Inject() when both are present on the same parameter.


@Component(options)

Target: class

Marks a class as a UI component. Internally applies @Injectable() and registers the component in ComponentRegistry using the provided selector.

function Component(options: ComponentOptions): ClassDecorator

ComponentOptions

interface ComponentOptions {
  selector: string;         // Required. E.g. 'app-user-card'
  template?: string;        // Inline HTML template
  templateUrl?: string;     // Path to external template file
  styles?: string[];        // Inline CSS strings
  styleUrls?: string[];     // Paths to CSS files
  inputs?: string[];        // Declared input property names
  outputs?: string[];       // Declared output event names
  description?: string;     // Documentation / AI context
  tags?: string[];          // Categorization tags
  aiGenerated?: boolean;    // Whether this component was AI-generated
  version?: string;         // Component version (default '1.0.0')
  author?: string;          // Author name
}
import { ComponentDecorator } from '@tagna/udiot';

@ComponentDecorator({
  selector: 'user-card',
  template: '<div class="card">{{ user.name }}</div>',
  styles: ['.card { border: 1px solid #ccc; padding: 16px; }'],
  description: 'Displays a user summary card',
  tags: ['user', 'card']
})
class UserCardComponent {
  constructor(private userService: UserService) {}
}

@Controller(options)

Target: class

Marks a class as an HTTP controller. Applies @Injectable() and stores route metadata. HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete) register individual route handlers on the controller.

function Controller(pathOrOptions?: string | ControllerOptions): ClassDecorator

ControllerOptions

interface ControllerOptions {
  path?: string;         // Base path prefix for all routes. E.g. '/api/users'
  middleware?: any[];    // Middleware applied to all routes in this controller
}
import { Controller, Injectable } from '@tagna/udiot';
import { Get, Post, Body, Param } from '@tagna/udiot';

@Controller('/api/users')
class UsersController {
  constructor(private userService: UserService) {}

  @Get('/')
  async listUsers() {
    return this.userService.findAll();
  }

  @Get('/:id')
  async getUser(@Param('id') id: string) {
    return this.userService.findById(id);
  }

  @Post('/')
  async createUser(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

@Module(options)

Target: class

Defines a module that groups providers, controllers, and components. Modules are the primary unit of application organization.

function Module(options?: ModuleOptions): ClassDecorator

ModuleOptions

interface ModuleOptions {
  imports?: any[];       // Other modules to import
  providers?: any[];     // Services / providers to register
  controllers?: any[];   // Controller classes
  components?: any[];    // UI component classes
  exports?: any[];       // Providers accessible to importing modules
}
import { Module } from '@tagna/udiot';

@Module({
  imports: [DatabaseModule],
  providers: [UserRepository, UserService],
  controllers: [UsersController],
  exports: [UserService]
})
class UserModule {}

Complete Example: UserModule

The example below wires UserRepository (singleton), UserService (depends on repository and a config token), and UserController into a @Module.

import {
  Injectable,
  Inject,
  Controller,
  Module,
  ServiceScope
} from '@tagna/udiot';
import { Get, Post, Body, Param } from '@tagna/udiot';
import { globalContainer } from '@tagna/udiot';

// --- Config ---
globalContainer.registerValue('USER_SERVICE_CONFIG', {
  maxPageSize: 100,
  defaultRole: 'viewer'
});

// --- Repository ---
@Injectable({ scope: ServiceScope.SINGLETON })
class UserRepository {
  private users: Map<string, User> = new Map();

  findById(id: string): User | undefined {
    return this.users.get(id);
  }

  findAll(): User[] {
    return Array.from(this.users.values());
  }

  save(user: User): void {
    this.users.set(user.id, user);
  }
}

// --- Service Config Interface ---
interface UserServiceConfig {
  maxPageSize: number;
  defaultRole: string;
}

// --- Service ---
@Injectable({ scope: ServiceScope.SINGLETON })
class UserService {
  constructor(
    private repo: UserRepository,
    @Inject('USER_SERVICE_CONFIG') private config: UserServiceConfig
  ) {}

  findById(id: string): User | undefined {
    return this.repo.findById(id);
  }

  findAll(page = 1): User[] {
    const all = this.repo.findAll();
    const pageSize = this.config.maxPageSize;
    return all.slice((page - 1) * pageSize, page * pageSize);
  }

  create(data: CreateUserDto): User {
    const user: User = {
      id: crypto.randomUUID(),
      role: this.config.defaultRole,
      ...data
    };
    this.repo.save(user);
    return user;
  }
}

// --- Controller ---
@Controller('/api/users')
class UserController {
  constructor(private userService: UserService) {}

  @Get('/')
  listUsers() {
    return this.userService.findAll();
  }

  @Get('/:id')
  getUser(@Param('id') id: string) {
    return this.userService.findById(id);
  }

  @Post('/')
  createUser(@Body() dto: CreateUserDto) {
    return this.userService.create(dto);
  }
}

// --- Module ---
@Module({
  providers: [UserRepository, UserService],
  controllers: [UserController],
  exports: [UserService]
})
class UserModule {}
From the canonical UDIOT public documentation.Try it in the playground ↗