Jak utworzyć fabrykę obiektów zachowującą typ
Utworzyłem następującą fabrykę obiektów, aby utworzyć instancje wszystkich moich interfejsów:
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");
Jak widać, ta fabryka umożliwia tworzenie instancji i mockowanie tych interfejsów. Głównym problemem jest to, że muszę wywołać tę funkcję z nazwą interfejsu ORAZ interfejsem, aby zachować typ w przypisaniach:
ObjectFactory.getInstance<SomeInterface>("SomeInterface")
Widziałem to pytanie , ale nie podoba mi się pomysł korzystania z Base
interfejsu. Co więcej, takie podejście również nie zachowuje tego typu.
Idealnie chciałbym użyć swojego podejścia, ale bez konieczności korzystania z interfejsu, to znaczy użyć tylko nazwy interfejsu.
Na marginesie: deklaracja Injectable
to hack, aby kod działał, najlepiej, gdybym mógł używać tylko nazwy implementacji, czyli:
let DEFAULT_IMPLEMENTATIONS = {
SomeInterface: Implementation
}
Odpowiedzi
Ponieważ masz ustaloną listę mapowań typu nazwa-typ, które chcesz wspierać, ogólne podejście polega na rozważeniu typu obiektu T
reprezentującego to mapowanie, a następnie w przypadku dowolnej obsługiwanej nazwy interfejsu K extends keyof T
będziesz miał do czynienia z funkcjami, które zwraca własność pod tą nazwą ... mianowicie funkcje typu () => T[K]
. Innym sposobem na powiedzenie tego jest to, że będziemy używać keyofi wyszukiwać typy, aby pomóc w nadaniu typów w Twojej fabryce.
Będziemy używać tylko konkretnego typu, takiego jak {"SomeInterface": SomeInterface; "Date": Date}
for T
, ale w dalszej części kompilator ma łatwiejszy czas, jeśli T
jest ogólny. Oto możliwa ogólna implementacja ObjectFactory
kreatora:
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;
}
}
}
Przebudowałem twoją wersję w coś, co kompilator może w większości zweryfikować jako bezpieczne dla typów, aby uniknąć asercji typu . Przejdźmy przez to.
makeFactory
Funkcja jest nazwą typu odwzorowania obiektu T
, i przyjmuje argument o nazwie DEFAULT_IMPLEMENTATIONS
typu { [K in keyof T]: () => T[K] }
. Jest to typ mapowany, którego klucze K
są takie same jak w przypadku, T
ale którego właściwościami są funkcje typu zero-arg, które zwracają wartość typu T[K]
. Możesz zobaczyć, jak DEFAULT_IMPLEMENTATIONS
wyglądało to w ten sposób: każda właściwość była funkcją o zerowym argumencie, zwracającą wartość odpowiedniego interfejsu.
W ramach implementacji funkcji tworzymy MOCK_IMPLEMENTATIONS
. Ta zmienna ma prawie taki sam typ jak, DEFAULT_IMPLEMENTATIONS
ale z właściwościami opcjonalnymi (na co wpływa modyfikator opcjonalności ?
in [K in keyof T]?
).
Funkcja zwraca samą fabrykę, która ma dwie metody:
getInstance
Metoda jest nazwą K
, typu nazwie interfejsu i wartość powrotna jest typu T[K]
, odpowiednie właściwości interfejsu. Zaimplementować to poprzez łączenie DEFAULT_IMPLEMENTATIONS
i MOCK_IMPLEMENTATIONS
przez obiekt rozprzestrzeniania i opisywania że to compositeInjectable
jest tego samego typu jak DEFAULT_IMPLEMENTATIONS
. Następnie indeksujemy go za pomocą interfaceName
i nazywamy go.
mockWithInstance
Sposób jest niespecyficzny K
, typ nazwą interfejsu i przyjmuje parametr typu K
(nazwa interface) i parametr typu T[K]
(odpowiedniego interfejsu).
Zobaczmy to w akcji:
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
To wszystko działa tak, jak myślę, że się spodziewasz. Wykonujemy ObjectFactory
dzwoniąc makeFactory
z żądanym DEFAULT_IMPLEMENTATIONS
przedmiotem. Tutaj dodałem adnotację, że SomeInterface
właściwość zwraca wartość typu SomeInterface
(w przeciwnym razie kompilator wywnioskuje, Implementation
która może być bardziej szczegółowa niż chcesz).
Następnie widzimy, że kompilator pozwala nam wywołać ObjectFactory.getInstance()
i ObjectFactory.mockWithInstance()
z odpowiednimi argumentami i zwrócić oczekiwane typy, a także działa w czasie wykonywania.
Link do kodu placu zabaw