Skip to main content

Decorators

DiCaf ships two decorator flavours. Choosing the wrong one — or mixing both — causes runtime errors that are hard to trace. This guide explains the difference, how to set each one up, and how to enforce consistency with ESLint.

For the full decorator API, see the Decorators reference.


Standard TC39 decorators, available in TypeScript 5.0+. No reflect-metadata polyfill required. Import from the main subpath:

import { Injectable, Lifetime, Scopes } from '@caffeine-projects/dicaf/decorators'

TypeScript config

{
"compilerOptions": {
"target": "ES2022"
}
}

experimentalDecorators must be absent or false. TypeScript 5 enables stage 3 decorators by default.

Declaring dependencies

Stage 3 decorators do not emit constructor parameter metadata. Dependencies must be declared explicitly in the @Injectable array:

@Injectable([Logger, Database])
class UserService {
constructor(private readonly logger: Logger, private readonly db: Database) {}
}

Legacy TypeScript decorators

Uses TypeScript's experimentalDecorators flag and the reflect-metadata polyfill. Import from the legacy subpath:

import { Injectable, Lifetime, Scopes } from '@caffeine-projects/dicaf/decorators/legacy'

TypeScript config

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

Setup

Install reflect-metadata and import it once at your application entry point before any DiCaf code runs:

npm install reflect-metadata
// main.ts — must be first
import 'reflect-metadata'

Declaring dependencies

With emitDecoratorMetadata: true, the compiler emits constructor parameter types as metadata. DiCaf reads this at startup so you do not need an explicit dependency list:

@Injectable()
class UserService {
// Logger and Database inferred automatically from the parameter types
constructor(private readonly logger: Logger, private readonly db: Database) {}
}

Do not mix both flavours

Stage 3 and legacy decorators are not compatible. Importing from both subpaths in the same project will produce undefined behaviour: metadata will be read by the wrong reader, decorators will silently no-op, or the container will fail to resolve bindings.

// wrong — never mix these two imports
import { Injectable } from '@caffeine-projects/dicaf/decorators'
import { Lifetime } from '@caffeine-projects/dicaf/decorators/legacy'

Pick one flavour per project and use it consistently across every file.


Enforcing consistency with ESLint

To prevent import-path mixing, add an import-x/no-restricted-paths (or equivalent) rule pointing at the subpath you are not using:

// eslint.config.js — stage 3 project: ban the legacy subpath
rules: {
'no-restricted-imports': ['error', {
patterns: ['@caffeine-projects/dicaf/decorators/legacy*'],
}],
}

// eslint.config.js — legacy project: ban the stage 3 subpath
rules: {
'no-restricted-imports': ['error', {
patterns: ['@caffeine-projects/dicaf/decorators', '!@caffeine-projects/dicaf/decorators/legacy'],
}],
}

Which flavour to choose

Stage 3Legacy
TypeScript version5.0+any
reflect-metadatanot requiredrequired
Explicit dep listrequiredoptional
Long-term standardyesno

Choose stage 3 for new projects. Use legacy only when migrating an existing codebase that already relies on experimentalDecorators and implicit injection.