cmd-johnson / deno-dependency-injector

Simple dependency injection for Deno TypeScript projects.
MIT License
13 stars 6 forks source link

Inject collection array? #1

Open justinmchase opened 3 years ago

justinmchase commented 3 years ago
interface IExample {
  name: string
}

@Injectable()
class A implements IExample {
  public name = 'A'
}

@Injectable()
class B implements IExample {
  public name = 'B'
}

@Bootstrapped()
class Main {
  constructor(private readonly services: IExample[]) {}
}

I need to be able to register and get all services exported as a certain type. Most other DI frameworks have a feature where you put the extra info onto the Injectable attribute.

For example typedi looks like this:

import { Token } from "typedi";

export interface IExample {
  name: string
}

export const ExampleToken = new Token<IExample>("example")
import { Service } from "typedi";
import { IExample, ExampleToken } from "./example.js";

@Service({ id: ExampleToken, multiple: true })
class A implements IExample {
  public name = "A"
}
cmd-johnson commented 3 years ago

Thank you for the suggstion, @justinmchase! As far as I can tell, this feature wouldn't be trivial to implement. At the very least it would require adding injection tokens, as I cannot get the generic array's item types out of TypeScript's reflection system. If I'm already adding tokens, I should probably also add a way to inject non-class types (i.e. arbitrary values) using these tokens. I'll need to give this some thought to find an API I'm happy with.

I haven't used typedi myself, but from looking at the documentation I don't understand why the multiple: true is defined on the @Service decorator instead of the token itself. What would happen when I decorate two classes with @Service, one with multiple: true and the other with multiple: false and then try to inject those?

Without having spent too much time thinking about this, the following approach might work:

So for your use-case it might look something like this:

interface IExample {
  name: string;
}

const IExampleToken = new MultiInjectionToken<IExample>("IExample");

@Injectable({ token: IExampleToken })
class A implements IExample {
  public name = "A";
}

@Injectable({ token: IExampleToken })
class B implements IExample {
  public name = "B";
}

@Bootstrapped()
class Main {
  constructor(@Inject({ token: IExampleToken }) private readonly services: IExample[]) {
    console.log(services.map(s => s.name)); // [ "A", "B" ]
  }
}

What do you think?

justinmchase commented 3 years ago

Those all seem perfectly reasonable, I can't think of a reason why isMulti is on the service either. I can't remember what it does if you don't specify multi, it either throws on the second addition or overwrites... both of which really don't make sense. Also its unclear what happens when you call Container.get(token) for a multi service (the first one? the last one? throw?) or if you have some with multi and some without...

So yeah I think I agree with you that it would make more sense to attach it to the token so that its always multi or not multi and have the one single resolve function return either a single instance or multiple based on the token... yeah seems way better that way.