タイプを維持するオブジェクトファクトリを作成する方法

Dec 23 2020

すべてのインターフェイスの実装をインスタンス化するために、次のオブジェクトファクトリを作成しました。

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
}

回答

1 jcalz Dec 23 2020 at 09:48

サポートしたい名前からタイプへのマッピングの固定リストがあるため、ここでの一般的なアプローチは、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] }。これは、キーがキーと同じであるが、プロパティがタイプの値を返すゼロ引数関数であるマップされたタイプです。既存のものが次のようになっていることがわかります。各プロパティは、対応するインターフェイスの値を返すゼロ引数関数でした。KTT[K]DEFAULT_IMPLEMENTATIONS

関数の実装内で、を作成しますMOCK_IMPLEMENTATIONS。この変数は、ほぼ同じタイプを有するDEFAULT_IMPLEMENTATIONSが、性質があると、オプション(選択性改質剤によって行われるよう?[K in keyof T]?)。

この関数は、ファクトリ自体を返します。これには、次の2つのメソッドがあります。

このgetInstanceメソッドはK、インターフェイス名のタイプであるジェネリックであり、戻り値はT[K]、対応するインターフェイスプロパティであるタイプです。マージDEFAULT_IMPLEMENTATIONSMOCK_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()適切な引数を持つと期待される型を返す、それはまた、実行時に動作します。


コードへの遊び場のリンク