2

How can I create a type-safe service registry in Typescript. Basically, a function that returns a concrete type based on the argument.

get("a") would return an object of type ServiceA.
get("b") would return an object of type ServiceB.

I followed the answer from here: https://stackoverflow.com/a/47098963/12116498 but am still getting typing errors.

I have reduced my problem to the following and added the type errors I am getting:

console.log("Test get");
console.log("Get foo from AlertsService:", get('AlertsService').foo);
console.log("Get foo from AlertsService:", get('ModalService').foo);
console.log("Get foo from AlertsService:", get('ModalService').bar);
 
function get<T extends keyof ServiceMapping>(serviceType: T): ServiceMapping[T] {
  const t: keyof ServiceMapping = serviceType;
 
  switch (t) {
    case 'AlertsService':
 
// TS2322: Type '{ type: "AlertsService"; foo: string; }' is not assignable to type 'ServiceMapping[T]'.
// Type '{ type: "AlertsService"; foo: string; }' is not assignable to type 'never'.
// The intersection 'AlertsService & ModalService' was reduced to 'never' because property 'type' has conflicting types in some constituents.
 
      return {
        type: 'AlertsService',
        foo: 'testing'
      };
    case 'ModalService':
     
// TS2322: Type '{ type: "ModalService"; foo: string; bar: string; }' is not assignable to type 'ServiceMapping[T]'.
// Type '{ type: "ModalService"; foo: string; bar: string; }' is not assignable to type 'never'.
// The intersection 'AlertsService & ModalService' was reduced to 'never' because property 'type' has conflicting types in some constituents.
     
      return {
        type: 'ModalService',
        foo: 'testing',
        bar: 'bar test'
      };
 
    default:
      throw new Error("nope");
  }
}
 
export type ServiceMapping = {
  AlertsService: AlertsService;
  ModalService: ModalService;
}
 
export type AlertsService = {
  type: 'AlertsService',
  foo: string
}
 
export type ModalService = {
  type: 'ModalService',
  foo: string,
  bar: string
}

It is important to have the return type be narrowly scoped and not a union of other types.

Ozan
  • 4,345
  • 2
  • 23
  • 35
Pait
  • 31
  • 2
  • Interesting. If you cast the return value to `never` it gets accepted. – Ozan Mar 19 '21 at 08:00
  • `function get(serviceType: keyof ServiceMapping): ServiceMapping[typeof serviceType] { ... }` but I have no explanation why this solution works. – kruschid Mar 19 '21 at 08:38
  • I believe that is because of design limitation. T might be much wider than keyof ServiceMapping. I think overloadings might help you – captain-yossarian from Ukraine Mar 19 '21 at 08:55
  • It is looks like it worked only in TypeScript v 3.3.333 – captain-yossarian from Ukraine Mar 19 '21 at 08:59
  • 1
    @captain-yossarian that's what I initially thought, too. However type widening is not accepted by the ts compiler here. If we try to invoke OPs function with `get("x")` the compiler complains. Same goes for `get<"AlertsService" | "ModalService" | "x">("x")`. A related question about extending unions: https://stackoverflow.com/questions/45745441/extending-union-type-alias-in-typescript – kruschid Mar 19 '21 at 16:25
  • @Ozan, Casting the return "as never" solves the problem. Thank you. It doesn't seem like an ideal solution. – Pait Mar 19 '21 at 17:39
  • 1
    @captain-yossarian, your suggestion does not result in a strong type. When I attempt to reference the "bar" from the ModalService, I get "bar does not exist on type AlertService". – Pait Mar 19 '21 at 17:39
  • Maybe this will help in the future https://github.com/microsoft/TypeScript/pull/43183 – captain-yossarian from Ukraine Mar 20 '21 at 09:46
  • @linda-paiste do you have an idea? – Ozan Mar 22 '21 at 23:11

1 Answers1

0

@kruschid please tell me why overloadings are bad


  type AlertsService = {
    type: 'AlertsService',
    foo: string,
  }

  type ModalService = {
    type: 'ModalService',
    foo: string,
    bar: string
  }

  function get<T extends 'AlertsService'>(arg: 'AlertsService'): AlertsService
  function get<T extends 'ModalService'>(arg: T): ModalService
  function get<T extends 'AlertsService' | 'ModalService'>(arg: T) {

    switch (arg) {
      case 'AlertsService':
        return {
          type: 'AlertsService',
          foo: 'testing'
        };
      case 'ModalService':
        return {
          type: 'ModalService',
          foo: 'testing',
          bar: 'bar test'
        };

      default:
        throw new Error("nope");
    }
  }


  const result = get('AlertsService') // AlertsService
  const result2 = get('ModalService') // ModalService