So erstellen Sie eine Object Factory, in der der Typ beibehalten wird
Ich habe die folgende Objektfactory erstellt, um Implementierungen aller meiner Schnittstellen zu instanziieren:
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");
Wie Sie sehen können, können Sie mit dieser Factory diese Schnittstellen instanziieren und verspotten. Das Hauptproblem ist, dass ich diese Funktion mit dem Namen der Schnittstelle UND der Schnittstelle aufrufen muss, um den Typ in den Zuweisungen beizubehalten:
ObjectFactory.getInstance<SomeInterface>("SomeInterface")
Ich habe diese Frage gesehen , aber die Idee, eine Base
Schnittstelle zu verwenden, gefällt mir nicht . Darüber hinaus behält dieser Ansatz den Typ auch nicht bei.
Im Idealfall möchte ich meinen Ansatz verwenden, ohne jedoch die Schnittstelle verwenden zu müssen, dh nur den Namen der Schnittstelle.
Randnotiz: Die Deklaration von Injectable
ist ein Hack, damit dieser Code funktioniert. Idealerweise möchte ich nur den Implementierungsnamen verwenden können, dh:
let DEFAULT_IMPLEMENTATIONS = {
SomeInterface: Implementation
}
Antworten
Da Sie eine feste Liste von Zuordnungen von Namen zu Typ haben, die Sie unterstützen möchten, besteht der allgemeine Ansatz darin, in Bezug auf den Objekttyp zu denken, der T
diese Zuordnung darstellt, und sich dann für jeden unterstützten Schnittstellennamen K extends keyof T
mit Funktionen zu befassen, die Geben Sie die Eigenschaft unter diesem Namen zurück ... nämlich Funktionen vom Typ () => T[K]
. Eine andere Möglichkeit, dies zu sagen, besteht darin, dass wir keyofTypen verwenden und nachschlagen , um Ihrer Fabrik Typen zu geben.
Wir werden nur einen konkreten Typ wie {"SomeInterface": SomeInterface; "Date": Date}
für verwenden T
, aber im Folgenden hat der Compiler eine einfachere Zeit, wenn er T
generisch ist. Hier ist eine mögliche generische Implementierung eines ObjectFactory
Herstellers:
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;
}
}
}
Ich habe Ihre Version in etwas umgestaltet, das der Compiler meistens als typsicher überprüfen kann, um Typzusicherungen zu vermeiden . Lass uns durchgehen.
Die makeFactory
Funktion ist im Objektzuordnungstyp generisch T
und verwendet ein Argument mit dem Namen DEFAULT_IMPLEMENTATIONS
type { [K in keyof T]: () => T[K] }
. Dies ist ein zugeordneter Typ, dessen Schlüssel K
mit denen von identisch sind T
, dessen Eigenschaften jedoch Null-Argument-Funktionen sind, die einen Wert vom Typ zurückgeben T[K]
. Sie können sehen, wie Ihre vorhandene DEFAULT_IMPLEMENTATIONS
Eigenschaft war: Jede Eigenschaft war eine Null-Argument-Funktion, die einen Wert der entsprechenden Schnittstelle zurückgibt.
Innerhalb der Funktionsimplementierung erstellen wir MOCK_IMPLEMENTATIONS
. Diese Variable hat fast den gleichen Typ wie, DEFAULT_IMPLEMENTATIONS
jedoch sind die Eigenschaften optional (wie durch den Modifikator ?
für die Optionalität in bewirkt [K in keyof T]?
).
Die Funktion gibt die Factory selbst zurück, die zwei Methoden hat:
Die getInstance
Methode ist generisch in K
, der Typ des Schnittstellennamens, und der Rückgabewert ist vom Typ T[K]
, der entsprechenden Schnittstelleneigenschaft. Ich implementiere dies, indem ich sie zusammenführe DEFAULT_IMPLEMENTATIONS
und MOCK_IMPLEMENTATIONS
über Objekte verteile und annotiere, dass dies compositeInjectable
der gleiche Typ ist wie DEFAULT_IMPLEMENTATIONS
. Dann indizieren wir mit dem interfaceName
und nennen es.
Die mockWithInstance
Methode ist auch generisch in K
, dem Typ des Schnittstellennamens, und akzeptiert einen Parameter vom Typ K
(den Schnittstellennamen) und einen Parameter vom Typ T[K]
(die entsprechende Schnittstelle).
Lassen Sie es uns in Aktion sehen:
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
Das alles funktioniert so, wie ich denke, dass Sie es erwarten. Wir machen ObjectFactory
durch den Aufruf makeFactory
mit dem gewünschten DEFAULT_IMPLEMENTATIONS
Objekt. Hier habe ich kommentiert, dass die SomeInterface
Eigenschaft einen Wert vom Typ zurückgibt SomeInterface
(andernfalls würde der Compiler schließen, Implementation
was spezifischer sein könnte, als Sie möchten ).
Dann können wir sehen, dass der Compiler uns mit den richtigen Argumenten aufrufen ObjectFactory.getInstance()
und ObjectFactory.mockWithInstance()
die erwarteten Typen zurückgeben lässt, und es funktioniert auch zur Laufzeit.
Spielplatz Link zum Code