jeffijoe / awilix

Extremely powerful Inversion of Control (IoC) container for Node.JS
MIT License
3.61k stars 135 forks source link

Is it possible to reinforce constructor param name/types? #334

Closed anthonyma94 closed 1 year ago

anthonyma94 commented 1 year ago

I've been trying to switch from InversifyJS and this has been the best alternative so far! I'm trying to solve the problem of losing automatic type safety in the constructor.

In Inversify I would have something like this:

class UserController {
  constructor(@inject(UserRepository) private repo: UserRepository) {}
}

UserRepository in inject is not a string, it is the class itself. I am mapping the class itself to the value, not a string representation of the class. That way it is very easy to make sure that 1) I am mapping the correct thing, and 2) any changes to the name of the class would throw errors at compile.

I can't figure out how to get this assurance in Awilix. register forces me to use a string, and constructors param names don't have type checking. I also understand parameter decorators aren't a thing in TS 5.0 (part of the reason I'm switching). How do you recommend I go about this problem?

jeffijoe commented 1 year ago

TL;DR: No, it is not possible with CLASSIC injection; it's somewhat possible with PROXY and createContainer<YourCradleType>()


This was a design decision from the start.

The problems I have with Inversify and @inject(UserRepository) are:

Additionally, interfaces are the most natural way to describe, well, an interface, and from what I can tell, in Inversify you don't get type safety when injecting interfaces (requires @inject(TYPES.UserRepository) where TYPES is a collection of symbols manually maintained. Therefore, the lack of type safety at this level in Awilix was acceptable due to what we gained.

In practice, you will rarely ever encounter type issues that you don't catch as you are writing/copy-pasting code, or with automated tests.

If you really feel that it's worth the trouble (some users do! I personally don't) you can use PROXY injection + a cradle type.

interface Cradle {
  userRepository: UserRepository // or IUserRepository if an interface!
}

const container = createContainer<Cradle>({ injectionMode: InjectionMode.PROXY })

// woah!
class Consumer {
  constructor({ userRepository }: Pick<Cradle, 'userRepository'>) {}
}

// typed!
container.register({ 
  userRepository: asClass(UserRepository),
  consumer: asClass(Consumer)
})

// safe!
container.resolve('consumer')

Mind you, however, that CLASSIC injection is probably the cleanest (no proxy magic) and it is also the most performant after the container is initialized. There is a slight overhead with the proxy (not significant), but worth mentioning.