typescript - 如何使对象工厂保持类型
问题描述
我创建了以下对象工厂来实例化所有接口的实现:
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}
for这样的具体类型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
type的参数{ [K in keyof T]: () => T[K] }
。这是一个映射类型,其键与 的键K
相同,T
但其属性是返回 type 值的零参数函数T[K]
。您可以看到您现有的DEFAULT_IMPLEMENTATIONS
情况是这样的:每个属性都是一个零参数函数,返回相应接口的值。
在函数实现内部,我们创建MOCK_IMPLEMENTATIONS
. 此变量具有几乎相同的类型,DEFAULT_IMPLEMENTATIONS
但属性是可选?
的(受中的可选修饰符影响[K in keyof T]?
)。
该函数返回工厂本身,它有两个方法:
该getInstance
方法是泛型的K
,接口名称的类型,返回值是类型T[K]
,对应的接口属性。我通过合并DEFAULT_IMPLEMENTATIONS
和MOCK_IMPLEMENTATIONS
通过对象传播来实现这一点,并注释它compositeInjectable
与DEFAULT_IMPLEMENTATIONS
. 然后我们用 the 索引它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()
andObjectFactory.mockWithInstance()
并返回预期的类型,并且它也可以在运行时工作。
推荐阅读
- jquery - 来自模型的 Django Jquery 自动填充数据
- java - 测试 JPA @Query 方法失败,但方法在生产中有效
- db2 - 如何在 db2 中使用加载游标为现有表的新创建定义插入空条目
- php - 使用通配符遍历数组以获取元素名称
- flutter - Flutter - 我怎样才能让这些容器像按钮一样工作?
- javascript - 使用 Broadway.js 的 MPEG4 流式传输
- accessibility - 如何为屏幕阅读器学习 IAccessible2?
- java - AWS SNS - 如何用等号解析这个 json 对象?
- python - 为什么我在通过 spotipy 登录我的 Spotify 时会收到错误消息?
- scala - Scala 与实例混合