Skip to content

Commit e234bd4

Browse files
committed
docs: explain abstract classes as di tokens
1 parent 2f9d939 commit e234bd4

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

content/fundamentals/dependency-injection.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,85 @@ export class CatsRepository {
208208
209209
While we directly use the string `'CONNECTION'` in the above examples for illustration purposes, for clean code organization, it's best practice to define tokens in a separate file, such as `constants.ts`. Treat them much as you would symbols or enums that are defined in their own file and imported where needed.
210210

211+
#### Interfaces and abstract classes
212+
213+
TypeScript types/interfaces are erased during compilation, so Nest can't reference them at runtime. This means an interface can describe the shape of a dependency, but it can't be used as a DI token by itself.
214+
215+
Since Nest resolves providers by runtime tokens, use a string or `Symbol` token when registering a provider for an interface:
216+
217+
```typescript
218+
export interface LoggerService {
219+
log(message: string): void;
220+
}
221+
222+
export const LOGGER_SERVICE = Symbol('LOGGER_SERVICE');
223+
224+
@Injectable()
225+
export class PinoLoggerService implements LoggerService {
226+
log(message: string) {
227+
// implementation details
228+
}
229+
}
230+
231+
@Module({
232+
providers: [
233+
{
234+
provide: LOGGER_SERVICE,
235+
useClass: PinoLoggerService,
236+
},
237+
],
238+
})
239+
export class AppModule {}
240+
```
241+
242+
To inject this provider, pass that token to the `@Inject()` decorator:
243+
244+
```typescript
245+
@Injectable()
246+
export class CatsService {
247+
constructor(
248+
@Inject(LOGGER_SERVICE)
249+
private readonly logger: LoggerService,
250+
) {}
251+
}
252+
```
253+
254+
Abstract classes, unlike interfaces, exist at runtime. You can use an abstract class as both the TypeScript contract and the DI token:
255+
256+
```typescript
257+
export abstract class LoggerService {
258+
abstract log(message: string): void;
259+
}
260+
261+
@Injectable()
262+
export class PinoLoggerService implements LoggerService {
263+
log(message: string) {
264+
// implementation details
265+
}
266+
}
267+
268+
@Module({
269+
providers: [
270+
{
271+
provide: LoggerService,
272+
useClass: PinoLoggerService,
273+
},
274+
],
275+
})
276+
export class AppModule {}
277+
```
278+
279+
With an abstract class token, constructor-based injection can use the abstract class type directly and doesn't require `@Inject()`:
280+
281+
```typescript
282+
@Injectable()
283+
export class CatsService {
284+
constructor(private readonly logger: LoggerService) {}
285+
}
286+
```
287+
288+
Use string or `Symbol` tokens when the runtime DI token should be decoupled from a class artifact. `Symbol` tokens are especially useful for libraries and larger applications because each symbol has a unique runtime identity, which helps avoid accidental collisions that can occur when unrelated providers use the same string token. When using a symbol token, export it from a shared file and reuse the same symbol instance wherever the provider is registered and injected. Use abstract classes when one artifact should act as both the contract and the runtime token, and you prefer simpler constructor injection. A plain interface is still a good choice when the type is only used for compile-time checking and no DI token is needed.
289+
211290
#### Class providers: `useClass`
212291

213292
The `useClass` syntax allows you to dynamically determine a class that a token should resolve to. For example, suppose we have an abstract (or default) `ConfigService` class. Depending on the current environment, we want Nest to provide a different implementation of the configuration service. The following code implements such a strategy.

0 commit comments

Comments
 (0)