@husky-di/decorator
v1.4.1
Published
`@husky-di/decorator` adds decorator support to husky-di. It translates TypeScript decorator metadata into `@husky-di/core` resolution behavior, so you can declare dependencies directly on constructor parameters.
Downloads
1,586
Readme
@husky-di/decorator
@husky-di/decorator adds decorator support to husky-di.
It translates TypeScript decorator metadata into @husky-di/core resolution behavior, so you can declare dependencies directly on constructor parameters.
You can think of it as a syntax layer on top of core:
- it makes dependency declaration feel more natural
- it does not replace the container itself, and lifecycle, middleware,
ref, anddynamicstill come from@husky-di/core
This package currently supports TypeScript experimental decorators only, not ES decorators. The reason is straightforward: husky-di depends on parameter decorators for constructor injection, and ES decorators do not provide that capability.
Is This The Right Package?
This package is a good fit when:
- you want dependencies to live directly on constructor parameters
- you do not want to hand-write wiring for every class
- you are comfortable using TypeScript experimental decorators and
reflect-metadata
If what you really want is:
- a low-level container with no decorator dependency:
see
../core/README.md - module import/export boundaries:
pair it with
../module/README.md
What You Get
@injectable()to mark classes as instantiable through decorator-aware resolution@inject()to declare service identifiers and resolve options for constructor parameters@tagged()as the low-level metadata decorator for custom abstractionsdecoratorMiddlewareto read injection metadata during class resolution- stable error exports such as
DecoratorExceptionandDecoratorErrorCodeEnum
Installation
pnpm add @husky-di/core @husky-di/decorator reflect-metadatareflect-metadata is a peer dependency.
At runtime, you need to load it first or provide a compatible Reflect metadata implementation.
TypeScript Configuration
Enable TypeScript experimental decorators and metadata emission:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Quick Start
The example below shows the most common setup: register the middleware once, then declare dependencies directly on constructor parameters.
import "reflect-metadata";
import {
createContainer,
createServiceIdentifier,
globalMiddleware,
} from "@husky-di/core";
import {
decoratorMiddleware,
inject,
injectable,
} from "@husky-di/decorator";
interface Logger {
log(message: string): void;
}
const ILogger = createServiceIdentifier<Logger>("ILogger");
@injectable()
class ConsoleLogger implements Logger {
log(message: string) {
console.log(`[log] ${message}`);
}
}
@injectable()
class UserService {
constructor(@inject(ILogger) private readonly logger: Logger) {}
getUser(id: string) {
this.logger.log(`load user: ${id}`);
return { id, name: "Ada" };
}
}
globalMiddleware.use(decoratorMiddleware);
const container = createContainer("AppContainer");
container.register(ILogger, { useClass: ConsoleLogger });
const userService = container.resolve(UserService);
console.log(userService.getUser("u-1"));In this example:
decoratorMiddlewarereads constructor parameter metadata@inject(ILogger)maps an interface dependency to a runtime-visible service identifier
With plain @husky-di/core, a common alternative is to resolve the dependency inside the class directly:
import { resolve } from "@husky-di/core";
class UserService {
private readonly logger = resolve(ILogger);
getUser(id: string) {
this.logger.log(`load user: ${id}`);
return { id, name: "Ada" };
}
}Adding It To An Existing Project
Register Middleware Globally
This is the recommended option for most applications:
import { globalMiddleware } from "@husky-di/core";
import { decoratorMiddleware } from "@husky-di/decorator";
globalMiddleware.use(decoratorMiddleware);That enables decorator-based constructor injection for all containers.
Register Middleware Locally
If you only want decorator support in one container, register it locally instead:
const container = createContainer("FeatureContainer");
container.use(decoratorMiddleware);Main APIs
@injectable()
Marks a class as instantiable by the decorator middleware and merges its parameter metadata into the internal metadata store.
import { injectable } from "@husky-di/decorator";
@injectable()
class UserService {}Key points:
- the same class cannot be decorated with
@injectable()more than once - parameters without explicit
@inject()are inferred fromdesign:paramtypes - if inference resolves to a primitive instead of a class, resolution fails immediately
@inject()
Explicitly declares the service identifier for a constructor parameter.
@injectable()
class UserService {
constructor(@inject(ILogger) private readonly logger: Logger) {}
}The supported service identifier kinds match core:
- class constructor
symbolstring
@tagged()
@tagged() is the lower-level metadata decorator.
It accepts the full InjectionMetadata object directly.
import { tagged, injectable } from "@husky-di/decorator";
@injectable()
class UserService {
constructor(
@tagged({ serviceIdentifier: ILogger, optional: true })
private readonly logger?: Logger
) {}
}This is useful when:
- you want to build a domain-specific custom decorator
- you want full control over the metadata instead of the
@inject()shorthand
Cases Where You Should Explicitly Use @inject()
Interface Types
Interfaces do not exist at runtime, so you must provide a runtime-visible service identifier explicitly.
interface Logger {
log(message: string): void;
}
const ILogger = createServiceIdentifier<Logger>("ILogger");
@injectable()
class UserService {
constructor(@inject(ILogger) private readonly logger: Logger) {}
}Primitive Types
Primitive types such as string, number, and boolean also need an explicit identifier.
const API_BASE_URL = Symbol("API_BASE_URL");
@injectable()
class ApiClient {
constructor(@inject(API_BASE_URL) private readonly baseUrl: string) {}
}When You Want To Override Inference
Even if the parameter type is a class, you should still write @inject() or @tagged() when you want to:
- use a different token
- enable
optional - enable
ref - enable
dynamic - change the
core.resolve()container scope
Injection Options
The decorator layer supports the same resolve options as core.resolve().
optional
Return undefined instead of throwing when the dependency is missing.
@injectable()
class UserService {
constructor(
@inject("auditLogger", { optional: true })
private readonly auditLogger?: { log(message: string): void }
) {}
}ref
Return a lazy reference, which is useful for deferred access or partially breaking circular dependencies.
import type { Ref } from "@husky-di/core";
@injectable()
class UserService {
constructor(
@inject(ILogger, { ref: true })
private readonly loggerRef: Ref<Logger>
) {}
run() {
this.loggerRef.current.log("run");
}
}dynamic
Return a dynamic reference whose .current value is re-resolved on every access.
import type { Ref } from "@husky-di/core";
@injectable()
class UserService {
constructor(
@inject(ILogger, { dynamic: true })
private readonly loggerRef: Ref<Logger>
) {}
}Prefer ref unless you specifically need to re-run resolution every time the value is read.
scope
Choose which core.resolve() container perspective the decorator should use for
that parameter.
import { ResolveContainerScopeEnum } from "@husky-di/core";
@injectable()
class DatabaseConsumer {
constructor(
@inject(IDatabaseOptions, { scope: ResolveContainerScopeEnum.origin })
private readonly options: { baseURL: string }
) {}
}This is useful when a parent-provided class should consume child-container overrides for a specific constructor parameter.
When Automatic Inference Works
Cases Where Type Inference Is Enough
If the parameter itself is a class, and that class is also marked with @injectable(), you can omit @inject():
@injectable()
class LoggerService {}
@injectable()
class UserService {
constructor(private readonly logger: LoggerService) {}
}Cases Where You Should Not Rely On Inference
Do not rely on automatic inference in these cases:
- the parameter type is an interface
- the parameter type is a primitive
- you need
optional - you need
ref - you need
dynamic - you want to bind the parameter to a different token than its runtime class
Relationship To core
@husky-di/decorator does not replace core.
It is a syntax layer built on top of it.
You still keep using:
createContainer()createServiceIdentifier()LifecycleEnumglobalMiddlewareresolve()/ref/dynamic
The decorator middleware only participates in the class-instantiation phase.
Registrations such as useValue, useFactory, and useAlias still follow normal core rules.
Common Pitfalls
Forgetting To Register decoratorMiddleware
If the middleware is not registered, the container does not read decorator metadata.
Forgetting To Import reflect-metadata
Without Reflect metadata at runtime, the implementation cannot access design:paramtypes.
Using Interfaces Or Primitives Without @inject()
That leaves the metadata incomplete or points inference at the wrong runtime identifier.
Applying @injectable() Twice To The Same Class
This throws E_DUPLICATE_INJECTABLE.
Using dynamic And ref Together
These options are mutually exclusive and throw E_CONFLICTING_OPTIONS.
Complete Example
import "reflect-metadata";
import {
createContainer,
createServiceIdentifier,
globalMiddleware,
type Ref,
} from "@husky-di/core";
import {
decoratorMiddleware,
inject,
injectable,
} from "@husky-di/decorator";
interface Config {
apiBaseUrl: string;
}
interface Logger {
log(message: string): void;
}
const IConfig = createServiceIdentifier<Config>("IConfig");
const ILogger = createServiceIdentifier<Logger>("ILogger");
@injectable()
class ConsoleLogger implements Logger {
log(message: string) {
console.log(message);
}
}
@injectable()
class ApiClient {
constructor(
@inject(IConfig) private readonly config: Config,
@inject(ILogger, { ref: true }) private readonly loggerRef: Ref<Logger>
) {}
getUser(id: string) {
this.loggerRef.current.log(`GET ${this.config.apiBaseUrl}/users/${id}`);
return { id, name: "Ada" };
}
}
globalMiddleware.use(decoratorMiddleware);
const container = createContainer("AppContainer");
container.register(IConfig, {
useValue: { apiBaseUrl: "https://api.example.com" },
});
container.register(ILogger, { useClass: ConsoleLogger });
const apiClient = container.resolve(ApiClient);
console.log(apiClient.getUser("u-1"));Related Docs
- container and resolution model:
../core/README.md - decorator behavior specification:
./docs/SPECIFICATION.md - module system:
../module/README.md
Local Development
pnpm build
pnpm test