如何使对象工厂维护类型

How to make an Object Factory maintaining the type

我创建了以下对象工厂来实例化我所有接口的实现:

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 的声明是使该代码正常工作的 hack,理想情况下,我希望能够仅使用实现名称,即:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

因为你有一个你关心支持的名称到类型映射的固定列表,这里的一般方法是根据表示这个映射的对象类型 T 来思考,然后对于任何支持的接口名称 K extends keyof T,您将处理 return 该名称的 属性 的函数...即类型 () => T[K] 的函数。另一种说法是,我们将使用 keyof and lookup types 来帮助为您的工厂提供类型。

我们只会为 T 使用像 {"SomeInterface": SomeInterface; "Date": Date} 这样的具体类型,但是如果 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;
    }
  }
}

我将你的版本重构为编译器可以验证为类型安全的版本,以避免 type assertions。让我们来看看它。

makeFactory 函数在对象映射类型 T 中是泛型的,并接受类型为 { [K in keyof T]: () => T[K] } 的名为 DEFAULT_IMPLEMENTATIONS 的参数。这是一个 mapped type,其键 KT 的键相同,但其属性是 return 类型 T[K] 的值的零参数函数。您可以看到您现有的 DEFAULT_IMPLEMENTATIONS 是这样的:每个 属性 都是一个零参数函数 returning 相应接口的值。

在函数实现中,我们创建 MOCK_IMPLEMENTATIONS。此变量与 DEFAULT_IMPLEMENTATIONS 具有几乎相同的类型,但属性为 optional(受 [K in keyof T]? 中的可选性修饰符 ? 影响)。

函数return是工厂本身,有两个方法:

getInstance方法在K接口名称的类型中是泛型,return值是T[K]类型,对应的接口属性。我通过 object spread 合并 DEFAULT_IMPLEMENTATIONSMOCK_IMPLEMENTATIONS 并注释此 compositeInjectableDEFAULT_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

这一切都如我所料。我们通过使用所需的 DEFAULT_IMPLEMENTATIONS 对象调用 makeFactory 来创建 ObjectFactory。在这里,我注释 SomeInterface 属性 return 是 SomeInterface 类型的值(否则编译器会推断出 Implementation 可能比您更具体喜欢)。

然后我们可以看到编译器允许我们使用适当的参数调用 ObjectFactory.getInstance()ObjectFactory.mockWithInstance() 并 returning 预期的类型,并且它在运行时也有效。


Playground link to code