Typescript iterative declaration merging

My current project involves creating a redux-like library using TypeScript. Here is an example of the basic action structure:

interface ActionBase {
  type: string;
  payload: any;
}

To customize actions for different types, I extend the base interface. For instance, for a button-click event, the setup would look like this:

interface ButtonClickAction extends ActionBase {
  type: 'BUTTON_CLICK';
  payload: {
    // Add metadata here
  };
}

In addition to defining these interfaces, I am also implementing helper functions:

function isInstanceOfButtonClick(action: ActionBase ): action is ButtonClickAction {
  return action.type === 'BUTTON_CLICK';
}

function buildButtonClickAction(payload): ButtonClickAction {
  return {
    type: 'BUTTON_CLICK',
    payload,
  };
}

The issue arises when dealing with multiple action types - around 20 in total. Is there a more efficient way to handle this? Each action requires:

  1. The specific type string ("BUTTON_CLICK")
  2. The payload type
  3. The action type itself (ButtonClickAction)
  4. A builder function (buildButtonClickAction)
  5. An instance check function (isInstanceOfButtonClick)

While it's possible to address items 1, 4, and 5 using classes or functions, I haven't found a streamlined approach for items 2 and 3. Currently, each action follows a similar pattern as shown below:

const KEY = 'BUTTON_CLICK';
namespace ButtonClick {
  export type Payload = {...}
  export interface Action extends ActionBase {
    type: typeof KEY;
    payload: Payload;
  }
}

let ButtonClick = makeActionValues<typeof KEY, ButtonClick.Payload, ButtonClick.Action>(KEY)

export default ButtonClick;

Is there a more elegant solution to tackle this challenge?

Answer №1

Have you considered implementing a function that generates a dictionary of Action factories, where each factory contains its own isInstance() and buildAction() methods for the corresponding type of Action? Here's an example:

interface ActionFactory<T extends string, P> {
  isInstance(action: Action): action is Action<T, P>;
  buildAction(payload: P): Action<T, P>;
}
interface Action<T extends string=string, P=any> {
  type: T,
  payload: P,
}

function getActionFactories<M extends object>(mappings: M): {[T in keyof M]: ActionFactory<T, M[T]>} {
  const ret: any = {};
  Object.keys(mappings).forEach((k: keyof M) => {
    type T = keyof M;
    type P = M[T];
    ret[k] = class Act {
      static isInstance(action: Action): action is Action<T, P> {
        return action.type === k;
      }
      static buildAction(payload: P): Action<T, P> {
        return new Act(payload);        
      }
      type: T = k;
      private constructor(public payload: P) { }
    }
  });
  return ret;
}

To use this function, create a mapping of action keys to payload types:

const _: any = void 0;
const ActionPayloads = {
  ButtonClick: _ as { whatever: string },
  SomeOtherAction: _ as { parameter: number },
  WhoKnows: _ as { notMe: boolean },
}

The above implementation may seem a bit cumbersome but it helps in keeping the code DRY; otherwise, key names would need to be specified twice. Once the mapping is done, call getActionFactories():

const Actions = getActionFactories(ActionPayloads);

The resulting Actions object acts like a namespace. For instance:

const buttonClick = Actions.ButtonClick.buildAction({ whatever: 'hello' });
const someOtherAction = Actions.SomeOtherAction.buildAction({ parameter: 4 });
const whoKnows = Actions.WhoKnows.buildAction({ notMe: false });
const randomAction = Math.random() < 0.33 ? buttonClick : Math.random() < 0.5 ? someOtherAction : whoKnows

if (Actions.WhoKnows.isInstance(randomAction)) {
  console.log(randomAction.payload.notMe);
}

Does this solution work for your needs?


Update 1

@darpa mentioned:

I'd like to retrieve the type of the resulting action.payload

To obtain the payload type for ButtonClick, you can refer to the ActionPayloads object in this manner:

const buttonClickPayload: typeof ActionPayloads.ButtonClick = {whatever: 'hello'};
const buttonClick = Actions.ButtonClick.buildAction(buttonClickPayload);

If you want the Actions object to expose this type, consider adding a phantom Payload property to ActionFactory:

interface ActionFactory<T extends string, P> {
  isInstance(action: Action): action is Action<T, P>;
  buildAction(payload: P): Action<T, P>;
  Payload: P; // phantom property
}

This way, you can access the payload type like so:

const buttonClickPayload: typeof Actions.ButtonClick.Payload = {whatever: 'hello'};
const buttonClick = Actions.ButtonClick.buildAction(buttonClickPayload);

Just remember not to actually utilize the value of Actions.ButtonClick.Payload, as it doesn't truly exist:

console.log(Actions.ButtonClick.Payload.whatever); // valid in TS but will cause errors at runtime.

Hope this clarification helps!

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

What is the best way to implement an asynchronous function using a for loop and APIs in Typescript?

Struggling with ensuring my function returns data only after completing all API calls and the for loop. getListingsImages(sessionID, mlsSearchCriteria){ this.http.get(this.laconiaBaseURL + "mls/search/" + sessionID + "?" +querySt ...

Ways to resolve the error message "Type 'Promise<{}>' is missing certain properties from type 'Observable<any>'" in Angular

Check out this code snippet: const reportModules = [ { url: '', params: { to: format(TODAY, DATE_FORMAT).toString(), from: format(TODAY, DATE_FORMAT).toString() } }, { url: 'application1', params: { to: for ...

The type '{ children: Element; }' cannot be assigned to the type 'IntrinsicAttributes & ReactNode'

Encountered this error: Type '{ children: Element; }' is not assignable to type 'IntrinsicAttributes & ReactNode'. export const withAppProvider = (Component: AppComponent) => { return function WrapperComponent(props: any) { ...

The style fails to load correctly upon the page's initial loading

I am utilizing <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="4d26282823603e212429283f0d7b63756378">[email protected]</a> in a <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="026c677a76 ...

Troubleshooting localhost issue with a Chrome extension in Visual Studio Code

When working in VS Code, I encountered an issue with launching my HTML AngularJS project on localhost. Every time I try to launch the project, I receive an error message stating "Failed to load resource: net::ERR_CONNECTION_REFUSED (http://localhost:8080/) ...

Navigating to different HTML pages by utilizing <a> elements in Angular 2

As a newcomer to Angular 2, I've decided to build my personal website using this framework. The main page of my website contains bio information, while the other page will feature blog content. Both pages will have a shared header and footer, but diff ...

Utilize passed props in components as well as Redux state information

Typically, you can access props sent by a parent to a child component on the child component itself. However, when using Redux in child components, the props sent by the parent are lost due to the use of the 'connect' method which maps the Redux ...

Looking for guidance on converting JS code to TypeScript? Let's tackle this TS test together!

I am facing the challenge of encapsulating a very complex SDK into a layer of code. I have attempted to utilize union and index types for this task, and below is a demo that I have created. How can I implement the bar method in TypeScript to pass the conso ...

SonarLint versus SonarTS: A Comparison of Code Quality Tools

I'm feeling pretty lost when it comes to understanding the difference between SonarLint and SonarTS. I've been using SonarLint in Visual Studio, but now my client wants me to switch to the SonarTS plugin. SonarLint is for analyzing overall pr ...

The system encountered difficulty handling a recursive structure

I am facing a challenge with a recursive JSON structure that needs to be stored as a series of maps with keys. The structure comprises flows and subflows that have references to each other. Here are the type declarations, noting that the issue lies in the ...

Importing TypeScript modules dynamically can be achieved without the need for Promises

I find myself in a scenario where the dynamic nature of these commands is crucial to prevent excessive loading of unnecessary code when executing specific command-line tasks. if (diagnostics) { require('./lib/cli-commands/run-diagnostics').run ...

Creating a Loading Sign with a Button Component in React

Request Description: In my form, I have a button that triggers a submission to the backend. While the request is processing, I want the button to display a loading indicator instead of the usual text. Once the request is complete, I need the form to disap ...

`The process of converting Typescript to ES5 through compiling/transpiling is encountering issues`

My current project involves using Webpack and integrating angular2 into the mix. To achieve this, I made adjustments to my setup in order to compile TypeScript. Following a resource I found here, my plan was to first compile TypeScript to ES6 and then tra ...

Issue with React TSX component in NextJs 14.0.4: Local MP3 files cannot be played, only external online MP3 files work

I have created a component that wraps around HTML audio and source tags. It functions perfectly when playing mp3 files from an external source, like this sound clip . However, it returns a GET 404 error when trying to access local mp3 files. Can anyone exp ...

The altered closure variable ts remains undetectable

Check out the live demonstration I made changes to the flag variable, but TypeScript did not recognize it. Could this be a coding issue? function fn () { let flag = true function f () { // alter the value of flag flag = false } for (let ...

When using React redux, creating a new store by passing it from a function like so: `<Provider store={configureStore()}>` results in the creation

I'm encountering a challenge and I am uncertain if this is the intended behavior. My approach involves creating a container wrapper that accepts the connected component as input: import store from 'somewhere' const ComponentWrapper = (Con ...

Transforming class attributes in Typescript

I am facing a situation where I need to alter the type of a variable that stores an object based on certain conditions. Here is the variable in question: class MyClass { public referrers: SelectItemGroup[]; } The issue arises when I only need to add a ...

Angular 5 is throwing an error stating that it cannot read the property 'text' of undefined

I have developed a question component where I have added some predefined questions. However, when I attempt to execute the application, it displays 'undefined' text. Below is my component code: import { Component, OnInit } from '@angular/c ...

Encountering error "module fibers/future not found" while creating a meteor method in typescript

While working on a Meteor method for async function in my project that combines Meteor with Angular2 using Typescript ES6, I encountered an error. The issue is related to a sync problem in the insert query when inserting data with the same name. To resolve ...

How to effectively handle null in Typescript when accessing types with index signatures unsafely

Why am I getting an error that test might be potentially undefined even though I've enabled strictNullCheck in my tsconfig.json file? (I'm unsure of the keys beforehand) const a: Record<string, {value: string}> = {} a["test"].va ...