如何在TypeScript中为此依赖项注入容器声明通用的映射类型

时间:2018-07-04 14:54:45

标签: typescript dependency-injection typescript-typings

我在TypeScript中提出了一个简单的异步依赖项注入容器:

function Container<P extends { [name: string]: { (c: any): Promise<any> } }>(provider: P) {
    const cache: { [name in keyof P]?: Promise<any> } = {};

    const container = function(name: keyof P) {
        if (!cache[name]) {
            cache[name] = provider[name](container);
        }

        return cache[name];
    }

    return container;
}

这是一个简单的用例示例:

class UserCache { }

class UserService {
    constructor(private cache: UserCache) { }
}

let container = Container({
    "cache": async c => new UserCache(),
    "user-service": async c => new UserService(await c("cache"))
});

container("user-service").then(service => {
    console.log(service);
});

因此,在这里简要解释一下期望的内容:Container函数基本上是一个“类型转换”,它采用服务定义(provider)的映射并返回工厂函数(也称为container。服务定义接收container实例,以便它们可以查找其依赖项,并返回解析为服务实例的Promise

我需要有关类型声明的帮助。

到目前为止,我已经对类型name进行了类型检查-生成的container函数仅接受provider中存在的键,到目前为止,效果很好。

缺少的是对container的调用的通用返回类型,因此依次可以解决嵌套的服务查找(例如await c("cache"))并正确进行类型检查。< / p>

有什么想法吗?

2 个答案:

答案 0 :(得分:3)

问题是使编译器为此container推断正确的类型:

let container = Container({
    "cache": async c => new UserCache(),
    "user-service": async c => new UserService(await c("cache"))
});

但这取决于传递给Container的参数的类型(这是一个对象常量),因此编译器会查看属性初始化程序的类型:

async c => new UserCache()

并发现c没有类型注释,因此必须推断其类型,并且约束条件是它与container是同一类型-糟糕,TypeScript不能很好地进行类型处理循环约束的推断。

c被推断为any,无法解决:

// let's start with imaginary type P which maps names to provided types

// and declare desired container type
type ContainerFunc<P> = <N extends keyof P>(n: N) => Promise<P[N]>;

// and its argument type
type ProviderType<P> = { [N in keyof P]: (c: ContainerFunc<P>) => Promise<P[N]> };


function Container<P>(provider: ProviderType<P>): ContainerFunc<P> {
    const cache: { [N in keyof P]?: Promise<P[N]> } = {};

    const container = function<N extends keyof P>(name: N): Promise<P[N]> {
        if (!cache[name]) {
            cache[name] = provider[name](container);
        }

        return cache[name];
    }

    return container;
}

class UserCache { cache: {} }

class UserService {
    constructor(private cache: UserCache) { }
}

let container = Container({
    "cache": async c => new UserCache(),
    "user-service": async c => new UserService(await c("cache"))
});

要删除圆度,必须删除c参数-它始终是container本身,让我们这样使用它:

type ContainerFunc<P> = <N extends keyof P>(n: N) => Promise<P[N]>;

type ProviderType<P> = { [N in keyof P]: () => Promise<P[N]> };

function Container<P>(provider: ProviderType<P>): ContainerFunc<P> {
    const cache: { [N in keyof P]?: Promise<P[N]> } = {};

    const container = function<N extends keyof P>(name: N): Promise<P[N]> {
        if (!cache[name]) {
            cache[name] = provider[name]();
        }

        return cache[name];
    }

    return container;
}

class UserCache { cache: {} }

class UserService {
    constructor(private cache: UserCache) { }
}

let container = Container({
    "cache": async () => new UserCache(),
    "user-service": async () => new UserService(await container("cache"))
});

但是现在container类型被推断为any,因为"user-service"的初始化程序在函数体中引用了container,从而阻止了该函数返回类型的推断。

您必须添加返回类型注释:

let container = Container({
    "cache": async () => new UserCache(),
    "user-service": async (): Promise<UserService> => new UserService(await container("cache"))
});

现在可以正常工作了

let container = Container({
    "cache": async () => new UserCache(),
    "user-service": async (): Promise<UserService> => new UserService(await container("user-service"))
});

// error: Argument of type 'UserService' is not assignable to parameter of type 'UserCache'.

我相信这是您所能得到的最好的。

更新,如您在注释中建议的那样,避免循环的另一种方法是在实施之前预先列出所有提供程序名称和提供的类型:

type ContainerFunc<P> = <N extends keyof P>(n: N) => Promise<P[N]>;

type ProviderType<P> = { [N in keyof P]: (c: ContainerFunc<P>) => Promise<P[N]> };


function Container<P>(provider: ProviderType<P>): ContainerFunc<P> {
    const cache: { [N in keyof P]?: Promise<P[N]> } = {};

    const container = function<N extends keyof P>(name: N): Promise<P[N]> {
        if (!cache[name]) {
            cache[name] = provider[name](container);
        }

        return cache[name];
    }

    return container;
}

class UserCache { cache: {} }

class UserService {
    constructor(private cache: UserCache) { }
}

interface ProviderTypes {
    cache: UserCache;
    "user-service": UserService;
}

let container = Container<ProviderTypes>({
    "cache": async c => new UserCache(),
    "user-service": async c => new UserService(await c("cache"))
});

答案 1 :(得分:2)

正如@artem所提到的,如果被推断的类型以任何方式引用自身,编译器将拒绝推断对象文字的类型(这是一个完全的限制,编译器甚至会提及这一点),因此对于当前结构,您拥有@ artem的答案就是最好的选择。

您可以考虑的一种重构是不一次性添加提供程序,而是在容器构建器类上使用add方法。 add的返回类型将返回一个新的容器构建器,该容器可以解析到那时为止添加的类型,因此新添加的服务只能使用已注册的服务(用柠檬制成柠檬水,这可能并不完全不好)避免循环引用)

实现看起来像这样:

function Container() {
    type ContainerFunc<P> = <N extends keyof P>(n: N) => P[N];
    class ContainerBuilder<T extends { [P in keyof T]: Promise<any> }> {
        public constructor(private provider: { [P in keyof T]: (c: any) => T[P] } = {} as any) {

        }
        public add<K extends string, V>(name: K, p: (c: ContainerFunc<T>)=> Promise<V>): ContainerBuilder<T & { [P in K]: Promise<V> }> {
            this.provider[name] = p;
            return this as any;
        }

        finish() : ContainerFunc<{ [P in keyof T]: T[P]}> {
            const provider = this.provider;
            const cache: { [name in keyof T]?: Promise<any> } = {};
            const container = function<K extends keyof T>(name: K) : T[K]  {
                if (!cache[name]) {
                    cache[name] = provider[name](container);
                }

                return cache[name] as any;
            }
            return container as any;

        }
    }

    return new ContainerBuilder()
}

class UserCache { }
class OfficeCache {}
class UserService {
    constructor(private cache: UserCache, private officeCache?: OfficeCache) { }
}


var c = Container()
    .add("user", async ()=> new UserCache())
    // all typed corectly, no any, the comented code below would be an error as office has not been registered yet
    .add("user-service", async c=> new UserService(await c("user") /* , await c("office") */)) 
    .add("office", async c=> new OfficeCache())
.finish();

let userService = c("user-service");