const myService = "MyService";
interface MyService {
doSth(): void;
}
class MyConsumer {
constructor(@inject(/* (1) */ myService) myService: /* (2) */ MyService) {
}
}
At (1), you can specify an arbitrary string. User of such a framework are not forced to use string constants, they could just use plain strings. If they do that, it will be very hard to rename those strings as they have no meaning besides being text.
At (2), you can specify an arbitrary type.
This makes it very hard to discover what you can import.
// A ServiceId combines an id and a type. The id is for display purposes only.
// A global counter could be used to make it actually unique.
const myService = new ServiceId<MyService>("MyService");
interface MyService {
doSth(): void;
}
class MyConsumer {
constructor(@inject(/* (1) */ myService) myService: /* (2) */ typeof myService.T) {
}
}
At (1), @inject
expects a ServiceRef<T>
instance. Users of this framework need to declare some const.
No user would accidentally use new ServiceId<MyService>("myService")
here.
At (2) you can still specify an arbitrary type - TypeScripts type system cannot enforce anything here.
However, an ESLint rule could check that for every @inject($x) $y: $t
, $t
must be typeof $x.T
.
This would effectively yield static type safety.
You could use this implementation of ServiceRef<T>
:
class ServiceRef<T> {
public get T() {
throw new Error("This property must not be used in non-type positions.");
}
constructor(public readonly id: string) {
}
}