在TypeScript中实现类型安全服务注册表

在TypeScript中实现类型安全服务注册表,typescript,Typescript,我希望有一个基于给定标识符(例如字符串或符号)返回对象实例的函数。在代码中,它可能看起来像: // define your services type ServiceA = { foo: () => string }; const ServiceA = { foo: () => 'bar' }; type ServiceB = { bar: () => number }; const ServiceB = { bar: () => 1 }; // register y

我希望有一个基于给定标识符(例如字符串或符号)返回对象实例的函数。在代码中,它可能看起来像:

// 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


有没有办法做这样的事?如果没有,从编译器的角度来看原因是什么?

如果您事先知道将从注册表返回的服务类型,则可以使用表示从字符串键到服务类型映射的类型:

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] {
  return ({
    ServiceA: ServiceA,
    ServiceB: ServiceB
  })[type];
}

// use it
const serviceA = getService('ServiceA'); 
serviceA.foo();  // works
下面是您如何使用它:

// register things
const registry = ServiceRegistry.init()
  .register('ServiceA', ServiceA)
  .register('ServiceB', ServiceB);
请注意,
registry
的类型是最后一个
register()
方法的返回值类型,该方法具有从类型键到服务对象的所有相关映射

const serviceA = registry.get('ServiceA');
serviceA.foo(); // works


我不确定您的要求,但也许您可以使用:


受的启发,您也可以使用JavaScript对象作为注册表的基础:

const serviceMap = {
    "ServiceA": ServiceA,
    "ServiceB": ServiceB,
};

function getService<K extends keyof typeof serviceMap>(serviceName:K): typeof serviceMap[K] {
    return serviceMap[serviceName];
}
除此之外,其他模块还可以通过扩展接口添加其他服务

declare global {
    interface ServiceMap {
        ["ServiceC"]: ServiceC,
    }
}

// register service "ServiceC"

也可以炒菜,但在我看来没有那么好的伸缩性。@BzumS是的,你是对的,但因为这是一个非常简单的解决方案,我想在这里列出它。我还添加了另一个受第一个建议启发的建议。很好,“静态”解决方案对我来说很好,正是我想要的!第一个解决方案仅适用于TS 3.3.333:(更新为适用于TS3.5+
const serviceMap = {
    "ServiceA": ServiceA,
    "ServiceB": ServiceB,
};

function getService<K extends keyof typeof serviceMap>(serviceName:K): typeof serviceMap[K] {
    return serviceMap[serviceName];
}
declare global {
    interface ServiceMap {
        ["ServiceA"]: ServiceA,
        ["ServiceB"]: ServiceB,
    }
}

function getService<K extends keyof ServiceMap>(serviceName:K): ServiceMap[K] {
    // Get service somehow.
}
declare global {
    interface ServiceMap {
        ["ServiceC"]: ServiceC,
    }
}

// register service "ServiceC"