Skip to main content

Decorators

Contexify provides a set of decorators that make it easy to work with dependency injection in TypeScript.

@injectable()

Marks a class as injectable, allowing Contexify to create instances of it.

Syntax:

function injectable(): ClassDecorator;

Example:

@injectable()
class UserService {
constructor() {
console.log('UserService created');
}

getUsers() {
return ['user1', 'user2', 'user3'];
}
}

// Now UserService can be created through Context
context.bind('services.UserService').toClass(UserService);
const userService = await context.get<UserService>('services.UserService');

@inject()

Injects a dependency by its binding key.

Syntax:

function inject(
bindingKey: string,
options?: InjectionOptions
): ParameterDecorator & PropertyDecorator;

Parameters:

  • bindingKey: The key of the binding to inject.
  • options (optional): Options for the injection.

Example:

@injectable()
class UserRepository {
findAll() {
return ['user1', 'user2', 'user3'];
}
}

@injectable()
class UserService {
constructor(@inject('repositories.UserRepository') private userRepo: UserRepository) {}

getUsers() {
return this.userRepo.findAll();
}
}

// Bind dependencies
context.bind('repositories.UserRepository').toClass(UserRepository);
context.bind('services.UserService').toClass(UserService);

// Resolve UserService (UserRepository is automatically injected)
const userService = await context.get<UserService>('services.UserService');
console.log(userService.getUsers()); // Output: ['user1', 'user2', 'user3']

Property Injection Example:

@injectable()
class UserService {
@inject('repositories.UserRepository')
private userRepo: UserRepository;

getUsers() {
return this.userRepo.findAll();
}
}

@inject.tag()

Injects all dependencies that match a specific tag.

Syntax:

namespace inject {
function tag(
tag: string,
options?: InjectionOptions
): ParameterDecorator & PropertyDecorator;
}

Parameters:

  • tag: The tag to match.
  • options (optional): Options for the injection.

Example:

@injectable()
class Logger {
constructor(private name: string) {}

log(message: string) {
console.log(`[${this.name}] ${message}`);
}
}

@injectable()
class Application {
constructor(@inject.tag('logger') private loggers: Logger[]) {}

run() {
this.loggers.forEach(logger => logger.log('Application started'));
}
}

// Bind multiple services with the same tag
context.bind('loggers.console').to(new Logger('console')).tag('logger');
context.bind('loggers.file').to(new Logger('file')).tag('logger');
context.bind('app').toClass(Application);

// Resolve the application (all services with the 'logger' tag are automatically injected)
const app = await context.get<Application>('app');
app.run();
// Output:
// [console] Application started
// [file] Application started

@inject.getter()

Injects a function that can be used to get the dependency later.

Syntax:

namespace inject {
function getter(
bindingKey: string,
options?: InjectionOptions
): ParameterDecorator & PropertyDecorator;
}

Parameters:

  • bindingKey: The key of the binding to inject.
  • options (optional): Options for the injection.

Example:

@injectable()
class ConfigService {
constructor(@inject.getter('config.database') private getDbConfig: Getter<any>) {}

async connectToDatabase() {
// Get the configuration only when needed
const dbConfig = await this.getDbConfig();
console.log(`Connecting to ${dbConfig.host}:${dbConfig.port}`);
}
}

// Bind configuration
context.bind('config.database').to({
host: 'localhost',
port: 5432,
username: 'admin',
password: 'secret'
});

context.bind('services.ConfigService').toClass(ConfigService);

// Resolve the service
const configService = await context.get<ConfigService>('services.ConfigService');
await configService.connectToDatabase(); // Output: Connecting to localhost:5432

@inject.view()

Injects a ContextView that tracks bindings matching a filter.

Syntax:

namespace inject {
function view(
filter: BindingFilter,
options?: InjectionOptions
): ParameterDecorator & PropertyDecorator;
}

Parameters:

  • filter: A function that filters bindings.
  • options (optional): Options for the injection.

Example:

@injectable()
class Plugin {
constructor(public name: string) {}

initialize() {
console.log(`Plugin ${this.name} initialized`);
}
}

@injectable()
class PluginManager {
constructor(
@inject.view(binding => binding.tags.has('plugin'))
private pluginView: ContextView<Plugin>
) {}

async initializePlugins() {
const plugins = await this.pluginView.resolve();
plugins.forEach(plugin => plugin.initialize());
}
}

// Bind plugins and manager
context.bind('plugins.logger').to(new Plugin('logger')).tag('plugin');
context.bind('plugins.auth').to(new Plugin('auth')).tag('plugin');
context.bind('plugins.cache').to(new Plugin('cache')).tag('plugin');
context.bind('managers.PluginManager').toClass(PluginManager);

// Resolve manager and initialize plugins
const pluginManager = await context.get<PluginManager>('managers.PluginManager');
await pluginManager.initializePlugins();
// Output:
// Plugin logger initialized
// Plugin auth initialized
// Plugin cache initialized

@config()

Injects configuration for the current binding.

Syntax:

function config(
propertyPath?: string | ConfigurationOptions
): ParameterDecorator & PropertyDecorator;

Parameters:

  • propertyPath (optional): The path to the configuration property, or configuration options.

Example:

@injectable()
class DatabaseService {
constructor(
@config('database.host') private host: string,
@config('database.port') private port: number
) {}

connect() {
console.log(`Connecting to database at ${this.host}:${this.port}`);
}
}

// Bind configuration
context.configure('services.DatabaseService').to({
database: {
host: 'localhost',
port: 5432
}
});

context.bind('services.DatabaseService').toClass(DatabaseService);

// Resolve the service
const dbService = await context.get<DatabaseService>('services.DatabaseService');
dbService.connect(); // Output: Connecting to database at localhost:5432

@intercept()

Applies interceptors to a method or class.

Syntax:

function intercept(
...interceptors: (Interceptor | Constructor<Interceptor>)[]
): MethodDecorator & ClassDecorator;

Parameters:

  • interceptors: One or more interceptors to apply.

Example:

// Define an interceptor
class LoggingInterceptor implements Interceptor {
intercept(invocationCtx: InvocationContext, next: () => ValueOrPromise<any>) {
console.log(`Calling ${invocationCtx.methodName} with args:`, invocationCtx.args);
const start = Date.now();
const result = next();

if (result instanceof Promise) {
return result.then(value => {
console.log(`${invocationCtx.methodName} completed in ${Date.now() - start}ms`);
return value;
});
}

console.log(`${invocationCtx.methodName} completed in ${Date.now() - start}ms`);
return result;
}
}

// Use the interceptor
@injectable()
class UserService {
@intercept(new LoggingInterceptor())
async findUsers() {
// Simulate database query
await new Promise(resolve => setTimeout(resolve, 100));
return ['user1', 'user2', 'user3'];
}
}

// Bind the service
context.bind('services.UserService').toClass(UserService);

// Resolve the service and call the method
const userService = await context.get<UserService>('services.UserService');
const users = await userService.findUsers();
// Output:
// Calling findUsers with args: []
// findUsers completed in 100ms

Class-level Interceptor Example:

// Apply interceptor to all methods in the class
@injectable()
@intercept(new LoggingInterceptor())
class UserService {
async findUsers() {
// ...
}

async createUser(name: string) {
// ...
}
}

Complete Example

Here's a complete example showing how to use the decorators together:

import {
Context,
injectable,
inject,
config,
intercept,
Interceptor,
InvocationContext,
ValueOrPromise
} from 'contexify';

// Define an interceptor
class LoggingInterceptor implements Interceptor {
intercept(invocationCtx: InvocationContext, next: () => ValueOrPromise<any>) {
console.log(`Calling ${invocationCtx.methodName}`);
const result = next();
console.log(`${invocationCtx.methodName} completed`);
return result;
}
}

// Define a configuration interface
interface DatabaseConfig {
host: string;
port: number;
username: string;
password: string;
}

// Define a repository
@injectable()
class UserRepository {
constructor(
@config('database') private dbConfig: DatabaseConfig,
@inject('services.LoggerService') private logger: LoggerService
) {}

findAll() {
this.logger.log(`Finding all users using ${this.dbConfig.host}:${this.dbConfig.port}`);
return ['user1', 'user2', 'user3'];
}
}

// Define a logger service
@injectable()
class LoggerService {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}

// Define a user service
@injectable()
@intercept(new LoggingInterceptor())
class UserService {
constructor(
@inject('repositories.UserRepository') private userRepo: UserRepository,
@inject.getter('config.appName') private getAppName: Getter<string>
) {}

async getUsers() {
const appName = await this.getAppName();
console.log(`Getting users for ${appName}`);
return this.userRepo.findAll();
}
}

// Create a context
const context = new Context('application');

// Bind services and configuration
context.bind('services.LoggerService').toClass(LoggerService);
context.bind('repositories.UserRepository').toClass(UserRepository);
context.bind('services.UserService').toClass(UserService);

context.bind('config.appName').to('MyApp');
context.configure('repositories.UserRepository').to({
database: {
host: 'localhost',
port: 5432,
username: 'admin',
password: 'secret'
}
});

// Use the services
async function run() {
const userService = await context.get<UserService>('services.UserService');
const users = await userService.getUsers();
console.log('Users:', users);
}

run().catch(err => console.error(err));