Как объявить универсальные сопоставленные типы для этого контейнера внедрения зависимостей в TypeScript - PullRequest
0 голосов
/ 04 июля 2018

Я придумал этот простой асинхронный контейнер внедрения зависимостей в 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 - это, по сути, «приведение типов», которое берет карту определений сервисов (a provider) и возвращает фабричную функцию, известную как container. Определения службы получают экземпляр container, чтобы они могли искать свои зависимости и возвращают Promise, который разрешается в экземпляре службы.

Мне нужна помощь с объявлениями типов.

Я дошел до проверки типа аргумента name - результирующая функция container принимает только те ключи, которые существуют в provider, пока, что хорошо.

Отсутствует общий тип возвращаемых вызовов на container, так что, в свою очередь, вложенные запросы на обслуживание, такие как await c("cache"), могут быть разрешены и проверены на правильность типа.

Есть идеи, как это сделать?

Ответы [ 2 ]

0 голосов
/ 05 июля 2018

Как уже упоминалось @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");
0 голосов
/ 04 июля 2018

Проблема в том, чтобы заставить компилятор выводить правильный тип для этого 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"))
});
...