0.2.3 • Published 5 years ago

ff-ioc v0.2.3

Weekly downloads
1
License
MIT
Repository
github
Last release
5 years ago

ff-ioc

Build Status

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'