微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何使对象工厂维护类型

如何解决如何使对象工厂维护类型

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

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")

我见过 this question,但我不喜欢使用 Base 界面的想法。此外,这种方法也不会保留类型。

理想情况下,我想使用我的方法但不必使用接口,即只使用接口的名称

旁注:Injectable 的声明是使代码工作的一种技巧,理想情况下,我希望能够仅使用实现名称,即:

let DEFAULT_IMPLEMENTATIONS = {
    SomeInterface: Implementation
}

解决方法

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

我们将只对 {"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;
    }
  }
}

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

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

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

函数返回工厂本身,它有两个方法:

getInstance 方法在K 中泛型,接口名称的类型,返回值类型为T[K],对应的接口属性。我通过通过 object spread 合并 DEFAULT_IMPLEMENTATIONSMOCK_IMPLEMENTATIONS 来实现这一点,并注释此 compositeInjectableDEFAULT_IMPLEMENTATIONS 的类型相同。然后我们用 interfaceName 对其进行索引并调用它。

mockWithInstance 方法在 K 中也是泛型的,接口名称的类型,并接受类型为 K(接口名称)的参数和类型为 { {1}}(对应的接口)。


让我们看看它的实际效果:

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 可能比您想要的更具体)。

然后我们可以看到编译器允许我们使用正确的参数调用 ImplementationObjectFactory.getInstance() 并返回预期的类型,并且它也可以在运行时工作。


Playground link to code

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。