Creating a reliable service registry in TypeScript that ensures type safety

Looking to create a function that can return an object instance based on a specified identifier, such as a string or symbol. The code example might appear as follows:

// defining services
type ServiceA = { foo: () => string };
const ServiceA = { foo: () => 'bar' };

type ServiceB = { bar: () => number };
const ServiceB = { bar: () => 1 };

// registering services with identifiers
registry.register('serviceA', ServiceA);
registry.register('serviceB', ServiceB);

// accessing a service by its type identifier
// crucial aspect is knowing the type!
const serviceA: ServiceA = registry.get('serviceA');
serviceA.foo();

Another key requirement is that ServiceA and ServiceB are not required to share an interface.

The main challenge in this scenario lies in determining the exact type of the value returned by registry.get.

An initial attempt was made using:

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');
  }
};

A similar approach was also tried using if, however, the compiler struggles to derive the concrete type of the returned value. When executing

const x = getService(ServiceType.A);
, the type of x ends up being ServiceA | ServiceB, whereas the desired outcome is ServiceA.

Is there a solution for achieving this functionality? If not, what limitations prevent this from a compiler perspective?

Answer №1

If you are aware of the services your registry will offer in advance, you can utilize a type that maps string keys to service types:

type ServiceMapping = {
  ServiceA: ServiceA;  
  ServiceB: ServiceB;
}

function getService<T extends keyof ServiceMapping>(type: T): ServiceMapping[T] {
  return ({
    ServiceA: ServiceA,
    ServiceB: ServiceB
  })[type];
}

// implementation
const serviceA = getService('ServiceA'); 
serviceA.foo();  // valid usage

If you need a registry to dynamically add and manage services while maintaining compile-time type checking, you can use a chain of registry objects. A constant registry object won't suffice because TypeScript does not allow mutating the type based on registered items. However, by returning a new type of object when calling the register() method, you can guide TypeScript accordingly. Here's an example:

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 update the object 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({});
  }
}

Here's how you would use it:

// register services
const registry = ServiceRegistry.init()
  .register('ServiceA', ServiceA)
  .register('ServiceB', ServiceB);

Notice that the type of registry is based on the return value of the last register() call, containing all mappings from type keys to service objects.

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

Link to Playground with code

Answer №2

Not entirely certain about your specific requirements, but perhaps you could consider using 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;
    }
}

Taking inspiration from the response by jcalz, another approach would be to use a JavaScript object as a base for the registry:

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

function getService<K extends keyof typeof serviceMap>(serviceName:K): typeof serviceMap[K] {
    return serviceMap[serviceName];
}

If there is a need to expand the service map in other modules, opting for a global interface might be beneficial. This mirrors what TypeScript employs for functionalities like addEventListener and potentially other functions.

declare global {
    interface ServiceMap {
        ["ServiceA"]: ServiceA,
        ["ServiceB"]: ServiceB,
    }
}

function getService<K extends keyof ServiceMap>>(serviceName:K): ServiceMap[K] {
    // Implement logic to retrieve service.
}

Subsequent modules can add further services by extending this interface.

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

// Registering service "ServiceC"

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Signature for a generic function that takes an input of one type and returns an output of a different type

Hello, I am currently facing an issue where I am trying to pass a function as an argument to another function and need to provide the type signature for that function. Initially, I attempted to solve this problem using the following code snippet: const fun ...

How to refresh a page in Angular Typescript to wait for HTTP calls from the backend

Looking at the code snippet below: The initial HTTP call retrieves multiple IDs of orderlines (items). For each ID, another HTTP call is made to reserve them. Afterward, the page needs to be updated to display the reserved items. When dealing with a larg ...

Angular 2 component downgrade issue: What causes the error when constructor parameters are involved? (SystemJS) Unable to resolve all parameters for (?)

Consider this line as an example: constructor(private elementRef: ElementRef, private zone: NgZone) {} In order for the downgrade to be successful without any errors, I must remove the parameters from the constructor. Otherwise, I encounter the follo ...

How can a string array object be passed as an argument into the constructor of a class when creating the object?

When instantiating a class object, what is the best way to pass a string array as an argument into the constructor? export class ManagerComponent { private selectedFilters: SelectedFilters; public ngOnInit() { this.selectedFilters = new ...

Using Angular: Inject a different function after unsubscribing from the Timer Observable

I'm currently developing an exam app and I'm facing an issue where the view controller is getting stuck in a loop after unsubscribing from the timer. The goal is to notify the user when their time is up and redirect them to a results page. If an ...

How can I display two slides at once in Ionic 2/3 on a wide screen?

I have been searching for a solution that will allow me to display 1 ion-slide if the device screen is small, but if it's larger, then I want to display 2 ion-slides. Unfortunately, I have not been able to find a suitable solution yet. As an example, ...

Modifying the version target of TypeScript code results in the TypeScript Compiler being unable to locate the module

After installing signalr via npm in Visual Studio 2019, I encountered an issue. When the target in my compiler options is set to ES6, I receive the error TS2307 (TS) Cannot find module '@microsoft/signalr.'. However, when I change the target to E ...

Using TypeScript with React: Updating input value by accessing event.target.nodeValue

As a newcomer to TypeScript, I am in search of an elegant solution for the following dilemma. I have a state variable named emailAddress, which is assigned a value from an input field. Additionally, I need the input field to display and update its value ba ...

Passing a boolean value from the parent Stepper component to a child step in Angular

I am facing an issue with passing the boolean value isNewProposal from a parent Angular Material stepper component to its children steps. Despite using @Input(), the boolean remains undefined in the child component, even after being assigned a value (true ...

Error message: Issue with transmitting system props in MUI v5 (Styled Components with TypeScript)

Recently delving into the world of Material UI, I encountered an issue: import React from 'react'; import { styled, Typography } from '@mui/material'; interface DescriptionProps { textTitle: string; type?: string; } const TitleSty ...

Exploring the Differences between Angular's Http Module and the Fetch API

While I grasp the process Angular uses for HTTP requests, I find myself leaning towards utilizing the Fetch API instead. It eliminates the need to subscribe and unsubscribe just for a single request, making it more straightforward. When I integrated it int ...

Steps to disable error TS2367: The comparison seems unintentional as there is no overlap between the types 'true' and 'false'

Is there a way to suppress or change this error message to a warning in my code? It's preventing my app from compiling. TS2367: This comparison appears to be unintentional because the types 'true' and 'false' have no overlap. C ...

Is there any advice for resolving the issue "In order to execute the serve command, you must be in an Angular project, but the project definition cannot be located"?

Are there any suggestions for resolving the error message "The serve command requires to be run in an Angular project, but a project definition could not be found."? PS C:\angular\pro\toitsu-view> ng serve The serve command requires to be ...

JS : Removing duplicate elements from an array and replacing them with updated values

I have an array that looks like this: let arr = ['11','44','66','88','77','00','66','11','66'] Within this array, there are duplicate elements: '11' at po ...

What is the best way to ensure that at least one checkbox is chosen?

I need to implement validation for checkboxes in this section without using a form tag. It is mandatory for at least one checkbox to be selected. <div *ngFor="let item of officeLIST"> <div *ngIf=" item.officeID == 1"> <input #off type ...

Organize and arrange all arrays within an object using JavaScript/TypeScript

Is there a more elegant way to sort every array in this object? Currently, I am doing it as shown below: this.userInfo = { Interests: [], Websites: [], Games: [], Locations: [], Companies: [], Projects: [], Schools: [], Stu ...

Generate a navigation route based on the hierarchical relationship between parent and child elements

Here is an array of objects: [ { "id": 1942, "label": "1", "url": "", "homepage": false, "visible": true, "order": 1 }, { "id": 1943 ...

What are the best practices for integrating phantom types with methods in TypeScript?

Take a look at the code snippet below which utilizes phantom types: const strlen = (str: string) => str.length; type Const<A, B> = { type: 'Const', value: A }; const Const = <A, B = never>(value: A): Const<A, B> => ({ ty ...

Getting the PlayerId after a user subscribes in OneSignal with Ionic2

Currently working on an app with Ionic2 and facing a challenge with retrieving the player id after a user subscribes in order to store it in my database. Any suggestions on how I can retrieve the unique player id of OneSignal users post-subscription? ...

TS2322 error: What does it mean when the type is both not assignable and assignable?

I've been delving into the world of generics with Java and C#, but TypeScript is throwing me for a loop. Can someone shed some light on this confusion? constructor FooAdapter(): FooAdapter Type 'FooAdapter' is not assignable to type 'A ...