ff-ioc
v0.2.3
Published
Fail-fast yet minimum IoC container for TypeScript
Downloads
11
Maintainers
Readme
ff-ioc
Fail-fast IoC container powered by typechecking from TypeScript
ff-ioc
aims to introduce a simple yet enforced way to declare dependency graph: as long as using well type-defined providers, no dependency mistake can be escaped from the TypeScript complier.
Get Started
Supposed there is a UserService
and a FriendService
, which can be defined as below:
interface UserService {
get(id: string): Promise<User | null>;
add(user: User): Promise<User>;
delete(id: string): Promise<void>;
getByIdList(idList: string[]): Promise<User[]>;
}
interface FriendService {
getFriendsOfUser(uid: string): Promise<User[]>;
}
FriendService
depends on the UserService
for retrieving user information. Based on the definition of this dependency, their provider functions can be defined as below:
type UserServiceProvider = (deps: {
// No dependency
}) => UserService;
type FriendServiceProvider = (deps: {
userService: UserService,
}) => FriendService;
We can now implement both service providers based on above type-defs:
const provideUserService: UserServiceProvider = ({}) => {
return {
async get(id) { ... },
async add(user) { ... },
async delete(id) { ... },
async getByIdList(idList) { ... },
}
};
const provideFriendService: FriendServiceProvider = ({
userService,
}) => {
return {
async getFriendsOfUser(uid) {
return await userService.getByIdList(
await _getFriendUidList(uid)
);
},
};
}
Use createContainer
to create an IoC container and bind all providers to it:
import createContainer from 'ff-ioc';
const container = createContainer({
friendService: provideFriendService,
userService: provideUserService,
});
container.friendService.getFriendsOfUser('xxxxxxx').then((users) => ...);
Concept of Fail-fast
The ability of fail-fast comes from TypeScript by the following type definition:
type ProviderMap<T extends {
[k: string]: any;
}> = {
[N in keyof T]: (deps: T) => T[N];
};
T
is the generic type of container. It is inferred from ProviderMap<T>
when calling createContainer(providerMap)
:
function createContainer<T>(providerMap: ProviderMap<T>): T;
This means when you have ProviderMap<T>
as the following type:
{
greeting: (deps: { greeter: Greeter }) => Greeting,
greeter: (deps: {}) => Greeter,
}
TypeScript will infer T
as the following type:
{
greeting: Greeting,
greeter: Greeter,
}
It then uses T
to declare the first parameter of associated provider functions, which builds up a junction between the container type and dependency type expected by each provider. If the container type is not a supertype of one of the provider dependency, there will be a compile error:
type Greeter = (msg: string) => void;
const container = createContainer({
// This is OK:
// greeter: ({}) => console.log,
// This is not correct
greeter: ({}) => console,
greeting: ({ greeter }: { greeter: Greeter }) => (name: string) => greeter(`Hello ${name}`),
});
// TypeScript Error: Type 'Console' provides no match for the signature '(msg: string): void'