我想有一个函数,它根据给定的标识符(例如字符串或符号)返回一个对象实例。在代码中,它看起来可能像:
// define your services
type ServiceA = { foo: () => string };
const ServiceA = { foo: () => 'bar' };
type ServiceB = { bar: () => number };
const ServiceB = { bar: () => 1 };
// register your services with identifier
registry.register('serviceA', ServiceA);
registry.register('serviceB', ServiceB);
// get a service by its type identifier
// the important thing here: I want to know the type!
const serviceA: ServiceA = registry.get('serviceA');
serviceA.foo();
进一步要求:ServiceA
和ServiceB
不共享界面左右,它们可能完全不同。
所示示例中的挑战是了解registry.get
返回的值的确切类型。
我尝试了一种天真的方式:
enum ServiceType {
A,
B
}
const getService = (type: ServiceType): ServiceA | ServiceB => {
switch (type) {
case ServiceType.A:
return ServiceA;
case ServiceType.B:
return ServiceB;
default:
throw new TypeError('Invalid type');
}
};
和if
也是一样但是编译器无法派生返回值的具体类型,当我执行const x = getService(ServiceType.A);
时,x的类型是ServiceA | ServiceB
,什么我希望看到的是ServiceA
。
有没有办法做这样的事情?如果没有,从编译器的角度来看是什么原因呢?
答案 0 :(得分:3)
如果您事先知道将从注册表中获取的服务类型,那么您可以使用表示从字符串键到服务类型的映射的类型:
type ServiceMapping = {
// first ServiceA is a string key, second is a type
ServiceA: ServiceA;
// first ServiceB is a string key, second is a type
ServiceB: ServiceB;
}
function getService<T extends keyof ServiceMapping>(type: T): ServiceMapping[T] {
const t: keyof ServiceMapping = type;
switch (t) {
case 'ServiceA':
return ServiceA;
case 'ServiceB':
return ServiceB;
default:
// following line will error if you left out a check
const exhaustivenessWitness: never = t;
throw new Error("Invalid type "+t)
}
}
// use it
const serviceA = getService('ServiceA');
serviceA.foo(); // works
如果你想要一个注册表,你可以在编译时继续添加和跟踪类型,你需要使用一系列注册表对象。您无法真正使用常量注册表对象,因为TypeScript不允许您根据注册的内容改变对象的类型。但是,当您调用其register()
方法时,您可以通过使注册表返回一个新的类型对象来帮助TypeScript,并且只保留返回的对象。像这样:
class ServiceRegistry<T> {
private constructor(private registry: T) {}
register<K extends string, S>(key: K, service: S): ServiceRegistry<Record<K, S> & T> {
// add service to registry and return the same object with a narrowed type
(this.registry as any)[key] = service;
return this as any as ServiceRegistry<Record<K, S> & T>;
}
get<K extends keyof T>(key: K): T[K] {
if (!(key in this.registry)) {
throw new Error('Invalid type' + key);
}
return this.registry[key];
}
static init(): ServiceRegistry<{}> {
return new ServiceRegistry({});
}
}
所以这就是你如何使用它:
// register things
const registry = ServiceRegistry.init()
.register('ServiceA', ServiceA)
.register('ServiceB', ServiceB);
请注意,registry
的类型是最后一个register()
方法的返回值的类型,该方法具有从类型键到服务对象的所有相关映射。
const serviceA = registry.get('ServiceA');
serviceA.foo(); // works
希望有所帮助;祝你好运!
答案 1 :(得分:0)
我不确定您的要求,但也许您可以使用overloads:
function getService(serviceName:"ServiceA"): typeof ServiceA;
function getService(serviceName:"ServiceB"): typeof ServiceB;
function getService(serviceName:string): object {
switch(serviceName) {
case "ServiceA":
return ServiceA;
case "ServiceB":
return ServiceB;
default:
return null;
}
}
受answer of jcalz的启发,您也可以使用JavaScript对象作为注册表的基础:
const serviceMap = {
"ServiceA": ServiceA,
"ServiceB": ServiceB,
};
function getService<K extends keyof typeof serviceMap>(serviceName:K): typeof serviceMap[K] {
return serviceMap[serviceName];
}