タイプを維持するオブジェクトファクトリを作成する方法
すべてのインターフェイスの実装をインスタンス化するために、次のオブジェクトファクトリを作成しました。
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");
ご覧のとおり、このファクトリでは、これらのインターフェイスのインスタンス化とモックを作成できます。主な問題は、インターフェイスの名前とインターフェイスを使用してこの関数を呼び出し、割り当てのタイプを保持する必要があることです。
ObjectFactory.getInstance<SomeInterface>("SomeInterface")
私はこの質問を見ましたが、Base
インターフェースを使用するという考えは好きではありません。さらに、そのアプローチはタイプも保持しません。
理想的には、自分のアプローチを使用したいと思いますが、インターフェイスを使用する必要はありません。つまり、インターフェイスの名前のみを使用します。
補足:の宣言Injectable
は、そのコードを機能させるためのハックです。理想的には、実装名のみを使用できるようにしたいと思います。つまり、次のようになります。
let DEFAULT_IMPLEMENTATIONS = {
SomeInterface: Implementation
}
回答
サポートしたい名前からタイプへのマッピングの固定リストがあるため、ここでの一般的なアプローチは、T
このマッピングを表すオブジェクトタイプの観点から考えることです。次に、サポートされているインターフェイス名についてK extends keyof T
、次のような関数を処理します。その名前のプロパティを返します...つまり、タイプの関数() => T[K]
。別の言い方をすれば、ファクトリに型を与えるために型を使用keyofしてルックアップするということです。
我々は唯一のような具体的な種類使用する{"SomeInterface": SomeInterface; "Date": Date}
ために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]?
)。
この関数は、ファクトリ自体を返します。これには、次の2つのメソッドがあります。
この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()
適切な引数を持つと期待される型を返す、それはまた、実行時に動作します。
コードへの遊び場のリンク