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

Unexpected directory structure for internal npm module

Here is the package.json I use for building and pushing to an internal package registry: { "name": "@internal/my-project", "version": "0.0.0", "description": "Test package", "type&quo ...

What is the method for implementing type notation with `React.useState`?

Currently working with React 16.8.3 and hooks, I am trying to implement React.useState type Mode = 'confirm' | 'deny' type Option = Number | null const [mode, setMode] = React.useState('confirm') const [option, setOption] ...

Issue regarding custom CSS implementation in Angular project

Being a French speaker, I apologize in advance for any mistakes I might make. I am also fairly new to Angular, so if you could provide detailed explanations in your responses, it would be greatly appreciated. Thank you. I am trying to import a custom CSS ...

Enhance the navigation scroll bar with a blur effect

I'm looking to create a navigation bar with a cool blur effect as you scroll. Everything seems to be working fine, except when I refresh the page the scrollbar position remains the same and window.pageYOffset doesn't give me the desired result. T ...

What is the best way to link function calls together dynamically using RXJS?

I am seeking a way to store the result of an initial request and then retrieve that stored value for subsequent requests. Currently, I am utilizing promises and chaining them to achieve this functionality. While my current solution works fine, I am interes ...

I am attempting to send an array as parameters in an httpservice request, but the parameters are being evaluated as an empty array

Trying to upload multiple images involves converting the image into a base64 encoded string and storing its metadata with an array. The reference to the image path is stored in the database, so the functionality is written in the backend for insertion. Ho ...

A keyboard is pressing on tabs and navigating through the app's contents in Ionic 3 on an Android device

I'm currently working on an IONIC 3 app and facing a challenge. When I tap on the ion search and the Keyboard pops up in ANDROID, it disrupts the layout by pushing all the content around. Original screen: Keyboard mode active: Things I've tri ...

Progressive series of observable conditions

The issue at hand: I am faced with the task of checking multiple conditions, some of which lead to the same outcome. Here is the current flow: First, I check if a form is saved locally If it is saved locally, I display text 1 to the user If not saved l ...

`Unable to update the checked prop in MUI switch component`

The value of RankPermission in the switchPermission function toggles from false to true, but for some reason, the MUI Switch component does not update in the browser. I haven't attempted any solutions yet and am unsure why it's not updating. I&ap ...

Issue deploying serverless: Module '/Users/.../--max-old-space-size=2048' not found

Everything is running smoothly on my serverless project locally when I use sls offline start. However, when I attempt to deploy it through the command line with serverless deploy, I encounter the following error stack. internal/modules/cjs/loader.js:1030 ...

NextJS: Route Handler encountering Method Not Allowed (405) error when trying to redirect

Current NextJs version is 13.4.3 I have set up a route handler to capture POST requests. For more information on route handlers, please refer to the documentation at [https://nextjs.org/docs/app/building-your-application/routing/router-handlers] In this ...

Radio buttons with multiple levels

Looking to implement a unique two-level radio button feature for a specific option only. Currently, I have written a logic that will display additional radio buttons under the 'Spring' option. However, the issue is that when it's selected, t ...

Animating the Click Event to Change Grid Layout in React

Can a grid layout change be animated on click in React? For instance, consider the following component: import { Box, Button, styled, useMediaQuery } from "@mui/material"; import Row1 from "./Row1"; import React from "react"; ...

Redux-Form's validations may become invisible due to the triggering of the UNREGISTERED_FIELD and REGISTER_FIELD actions, both of which occur twice

In my application using React 16 and Redux-form 7, I'm encountering an issue with field validation. Whenever I change the value of a field, the UPDATE_SYNC_ERRORS action is triggered and correctly adds syncErrors to the state. However, after this, two ...

What is the best way to compare two TypeScript object arrays for equality, especially when some objects may have multiple ways to be considered equivalent

Currently, I am in the process of developing a cost function for a game where players are given a set of resources in their hand. The resources can be categorized into different types such as Fire, Air, Water, Earth, Good, Evil, Law, Chaos, and Void. Thes ...

Named functions in Typescript within functional components are the best practice for improving

How can I implement handleFoo using MyType['foo']? type MyType { foo: () => void } const Comp: React.FunctionComponent<{}> = () => { function handleFoo() {} return ... } I'm looking for a solution that doesn't inv ...

What are the solutions for handling undefined data within the scope of Typescript?

I am encountering an issue with my ngOnInit() method. The method fills a data list at the beginning and contains two different logic branches depending on whether there is a query param present (navigating back to the page) or it's the first opening o ...

"React's FC generic is one of its most versatile features

I'm currently working on a component that can render either a router Link or a Button based on the provided props. Here is the code snippet I have: import React from 'react'; import Button from '@material-ui/core/Button'; import { ...

I am looking for a guideline that permits me to restrict the use of a form validation tool

We have developed our own version of the Validators.required form-validator that comes with Angular 7, but now we need to switch to using CustomValidators.required. To enforce this change, we are considering banning the use of the old Validators.required b ...

Using a try block inside another try block to handle various errors is a common practice in JavaScript

In an effort to efficiently debug my code and identify the location of errors, I have implemented a try-catch within a try block. Here is a snippet of the code: for (const searchUrl of savedSearchUrls) { console.log("here"); // function will get ...