CodePark/ questions / TypeScript on the server
START PRACTICINGPRACTICE
Backend question · TypeScript › TypeScript on the server

How does NestJS know what to inject into a constructor when TypeScript types are erased? Why can't an interface be used as the injection token, and how do TypeScript 5.0 standard decorators affect this?

@Injectable()
export class UsersService {
  constructor(private readonly repo: UsersRepository) {}
}

// roughly what tsc emits with emitDecoratorMetadata:
UsersService = __decorate([
  Injectable(),
  __metadata("design:paramtypes", [UsersRepository]),
], UsersService);

NestJS reads metadata that the TypeScript compiler writes into the JavaScript output. When experimentalDecorators and emitDecoratorMetadata are enabled, tsc emits a design:paramtypes metadata entry for each decorated class. This entry lists the runtime constructors of the constructor's parameters. Nest reads it through the reflect-metadata library. Then it finds each class in its container and injects it.

An interface has no runtime value, because types are erased. So tsc records its parameter as Object, and Nest cannot resolve it. Instead, use one of these:

  • A string or symbol token with @Inject(TOKEN).
  • An abstract class, which does exist at runtime.

TypeScript 5.0 supports the TC39 standard decorators when experimentalDecorators is off. They differ from the legacy ones:

  • They have a different API.
  • They have no parameter decorators.
  • emitDecoratorMetadata does not apply to them.

So NestJS still relies on the legacy settings.

  • emitDecoratorMetadata makes tsc emit design:paramtypes for decorated classes.
  • Nest reads that metadata through reflect-metadata to resolve dependencies.
  • Interfaces are erased and appear as Object, so use @Inject(token) or an abstract class.
  • TS 5.0 standard decorators emit no metadata and have no parameter decorators.