Как сделать фабрику объектов, поддерживающую тип
Я создал следующую фабрику объектов для создания экземпляров реализаций всех моих интерфейсов:
interface SomeInterface {
get(): string;
}
class Implementation implements SomeInterface {
constructor() {}
get() {
return "Hey :D";
}
}
type Injectable = {
[key: string]: () => unknown;
};
// deno-lint-ignore prefer-const
let DEFAULT_IMPLEMENTATIONS: Injectable = {
SomeInterface: () => new Implementation(),
};
let MOCK_IMPLEMENTATIONS: Injectable = {};
class Factory {
static getInstance(interfaceName: string, parameters: unknown = []) {
if (MOCK_IMPLEMENTATIONS[interfaceName])
return MOCK_IMPLEMENTATIONS[interfaceName]();
return DEFAULT_IMPLEMENTATIONS[interfaceName]();
}
static mockWithInstance(interfaceName: string, mock: unknown) {
MOCK_IMPLEMENTATIONS[interfaceName] = () => mock;
}
}
export const ObjectFactory = {
getInstance<T>(name: string): T {
return Factory.getInstance(name) as T;
},
mockWithInstance: Factory.mockWithInstance,
};
const impl = ObjectFactory.getInstance<SomeInterface>("SomeInterface");
Как видите, эта Factory позволяет создавать экземпляры этих интерфейсов и имитировать их. Основная проблема заключается в том, что я должен вызывать эту функцию с именем интерфейса И интерфейса, чтобы сохранить тип в назначениях:
ObjectFactory.getInstance<SomeInterface>("SomeInterface")
Я видел этот вопрос , но мне не нравится идея использовать Base
интерфейс. Более того, такой подход не сохраняет тип.
В идеале я хотел бы использовать свой подход, но без использования интерфейса, то есть использовать только имя интерфейса.
Боковое примечание: объявление Injectable
- это взлом, чтобы заставить этот код работать, в идеале я хотел бы иметь возможность использовать только имя реализации, то есть:
let DEFAULT_IMPLEMENTATIONS = {
SomeInterface: Implementation
}
Ответы
Поскольку у вас есть фиксированный список сопоставлений имен и типов, которые вы заботитесь о поддержке, общий подход здесь состоит в том, чтобы думать в терминах типа объекта, T
представляющего это сопоставление, а затем для любого поддерживаемого имени интерфейса K extends keyof T
вы будете иметь дело с функциями, которые вернуть свойство с этим именем ... а именно функции типа () => T[K]
. Другими словами, мы будем использовать keyofтипы поиска и поиска, чтобы помочь предоставить типы вашей фабрике.
Мы будем использовать только конкретный тип, например {"SomeInterface": SomeInterface; "Date": Date}
for T
, но в дальнейшем компилятору будет проще, если он T
будет общим. Вот возможная общая реализация ObjectFactory
производителя:
function makeFactory<T>(DEFAULT_IMPLEMENTATIONS: { [K in keyof T]: () => T[K] }) {
const MOCK_IMPLEMENTATIONS: { [K in keyof T]?: () => T[K] } = {};
return {
getInstance<K extends keyof T>(interfaceName: K) {
const compositeInjectable: typeof DEFAULT_IMPLEMENTATIONS = {
...DEFAULT_IMPLEMENTATIONS,
...MOCK_IMPLEMENTATIONS
};
return compositeInjectable[interfaceName]();
},
mockWithInstance<K extends keyof T>(interfaceName: K, mock: T[K]) {
MOCK_IMPLEMENTATIONS[interfaceName] = () => mock;
}
}
}
Я преобразовал вашу версию во что-то, что компилятор может в основном проверить как безопасное по типу, чтобы избежать утверждений типа . Давайте пройдемся по нему.
Эта makeFactory
функция является универсальной для типа сопоставления объектов T
и принимает аргумент с именем DEFAULT_IMPLEMENTATIONS
типа { [K in keyof T]: () => T[K] }
. Это сопоставленный тип , ключи которого K
такие же, как ключи , T
но свойства которого являются функциями с нулевым аргументом, возвращающими значение типа T[K]
. Вы можете увидеть, как выглядело ваше существующее DEFAULT_IMPLEMENTATIONS
: каждое свойство было функцией с нулевым аргументом, возвращающей значение соответствующего интерфейса.
Внутри реализации функции мы создаем MOCK_IMPLEMENTATIONS
. Эта переменная имеет почти тот же тип, DEFAULT_IMPLEMENTATIONS
но со свойствами, которые являются необязательными (что определяется модификатором необязательности ?
в [K in keyof T]?
).
Функция возвращает саму фабрику, у которой есть два метода:
getInstance
Метод является общим в K
, типа имени интерфейса, а возвращаемое значение имеет тип T[K]
, соответствующий интерфейс собственности. Я осуществить это путем слияния DEFAULT_IMPLEMENTATIONS
и с MOCK_IMPLEMENTATIONS
помощью объекта распространения , и аннотирования , что это compositeInjectable
имеет тот же тип , как DEFAULT_IMPLEMENTATIONS
. Затем мы индексируем его с помощью interfaceName
и вызываем.
mockWithInstance
Метод также родовой в K
, тип имени интерфейса, и принимает параметр типа K
(имя интерфейса), и параметр типа T[K]
(соответствующий интерфейс).
Посмотрим на это в действии:
const ObjectFactory = makeFactory({
SomeInterface: (): SomeInterface => new Implementation(),
Date: () => new Date()
});
console.log(ObjectFactory.getInstance("SomeInterface").get().toUpperCase()); // HEY :D
ObjectFactory.mockWithInstance("SomeInterface", { get: () => "howdy" });
console.log(ObjectFactory.getInstance("SomeInterface").get().toUpperCase()); // HOWDY
console.log(ObjectFactory.getInstance("Date").getFullYear()); // 2020
Все работает так, как я думаю. Делаем ObjectFactory
путем звонка makeFactory
с нужным DEFAULT_IMPLEMENTATIONS
объектом. Здесь я отметил, что SomeInterface
свойство возвращает значение типа SomeInterface
(в противном случае компилятор определит, Implementation
что может быть более конкретным, чем вы хотели бы).
Затем мы видим, что компилятор позволяет нам вызывать ObjectFactory.getInstance()
и ObjectFactory.mockWithInstance()
с правильными аргументами и возвращать ожидаемые типы, а также он работает во время выполнения.
Ссылка на игровую площадку на код