Implementing tagged actions and an array of reducers in Typescript

I've been tackling TypeScript and delving into code heavily influenced by the Redux way of working, (action, state) => state.

I'm striving to use TypeScript in the strictest manner possible. The issue I'm encountering is best illustrated with an example.

Initially, I define an enum detailing the action types:

enum Categories {
  CAT_A = 'categoryA',
  CAT_B = 'categoryB'
}

Then, I outline distinct fields for each type:

type Data = {
  [Categories.CAT_A]: {
    foo: string;
  },
  [Categories.CAT_B]: {
    bar: number;
  }
}

Next, I establish my action types:

type Action<T extends Categories> = {
  type: T;
  id: string;
  additionalField: boolean;
} & Data[T];

type AllActions = { [T in Categories]: Action<T> }[Categories]

Following that, here's the reducer:

type State = { somevar: string }; // just an example

const dataReducer = (action: AllActions, state: State): State => {
  switch (action.type) {
    case Categories.CAT_A:
      action.foo; /* correct type */
      break;
    case Categories.CAT_B:
      action.bar; /* correct type */
      break;
  }
  return {...state};
}

So far, everything seems to be going smoothly!

In reality, dealing with numerous actions can make the switch statement messy.

Hence, I experimented with this alternative:

const subReducers: { [K in Categories]: (action: Action<K>, state: State) => State } = {
  [Categories.CAT_A]: (action, state) => {
    // accurate action type here
    return {...state};
  },
  [Categories.CAT_B]: (action, state) => ({...state})
}

This approach works wonders as most "subReducers" are concise. It also ensures I handle any future actions correctly.

However, when trying to write the actual reducer for this setup:

const reducer2 = (action: AllActions, state: State) => {
  const subReducer = subReducers[action.type]; 
  const newState = subReducer(action, state); //error
  return newState;
}

The error here stems from the fact that action.type isn't constrained to a specific type, causing ambiguity with subReducer. While casting resolves the issue, I'm seeking a solution that doesn't necessitate it. Just as a thought exercise.

If anyone has ideas for a highly type-safe solution that allows me to split my reducer into smaller parts like in the aforementioned example, feel free to share.

Feel free to modify the type definitions or structure, but keep in mind that robust type safety and inference are key objectives here.

Playground Link

Answer №1

The issue lies in the fact that the minireducer will encompass all signatures, making it non-callable.

Fortunately, there is a new feature in TypeScript 3.3 that relaxes this limitation. However, your code may not function properly as it expects the first argument to be an intersection of all arguments within the signature union.

To address this, one workaround is to introduce an additional function call, assuring the compiler that the object has the correct signature:

const reducer2 = (action: Actions, state: State) => {
    // Ensure that 'r' must have a property 'K' which is a function accepting Action<K>
    function call<K extends Tags>(o: Action<K>, r: Record<K, (action: Action<K>, state: State) => State>, state: State) {
        r[o.type](o, state); // 'r[o.type]' is  (action: Action<K>, state: State) => State and can be called with an Action<K>
    }
    const newState = call(action, minireducers, state);
    return newState;
}

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

Union template literal types are preprended in Typescript

Is there a method to generate specific string types from existing string types with a designated prefix? It's better to acknowledge that it doesn't exist than to dwell on this concept. type UserField = "id" | "name"; type Post ...

Using TypeScript to Extract Keys from an Array

Is it possible to mandate the keys of an interface to come from an array of strings: For instance, consider the following array: const myArray = ['key1', 'key2']; I aim to define a new interface named MyInterface that would require al ...

How do I modify the local settings of the ngx-admin datepicker component to show a Turkish calendar view?

Looking for tips on customizing the new datepicker component in Nebular ngx-admin. Specifically, I want to change the local settings to display the calendar as Turkish. Explored the library but still seeking alternative methods. Any suggestions? ...

Enhance the capabilities of Playwright's locator by integrating additional helper functions

I'm looking to enhance the functionality of Playwright's Locator by creating a customized class with some additional utility functions. The goal is for the behavior of this new class to remain identical to that of the original locator provided b ...

Using dangerouslySetInnerHTML in ReactJS can potentially strip away data attributes

After adding a data-test attribute to the a anchor tag within an HTML string and inserting it using dangerouslySetInnerHTML, I noticed that the data attributes are somehow being stripped out. Is there a way to prevent this from happening? These attribute ...

Automatically create index.d.ts type definitions from a TypeScript module with just a few clicks

If I have a TypeScript module named my-function.ts with the following code : export function myFunction (param: number): number { return param } When this module is compiled to JavaScript, it loses its type definitions. Is there a way to automatically ge ...

``Error Message: TypeORM - could not establish database connection

I encountered an issue while running my project built with Typescript, Typeorm, and Express. The error message received when running the dev script was: connectionNotFoundError: Connection "default" was not found The content of my ormconfig.json ...

What is a creative way to design a mat-radio-group without traditional radio buttons?

I am looking to create a component that offers users a list of selections with the ability to make only one choice at a time. The mat-radio-group functionality seems to be the best fit for this, but I prefer not to display the actual radio button next to t ...

How can I move the cursor to the beginning of a long string in Mat-autocomplete when it appears at the end?

I'm struggling to figure out how to execute a code snippet for my Angular app on Stack Overflow. Specifically, I am using mat-autocomplete. When I select a name that is 128 characters long, the cursor appears at the end of the selected string instead ...

Angular 2 file upload encountering CORS issue leading to 401 unauthorized error

Encountered a "401 Unauthorized" error in Chrome and Firefox while attempting to upload files using angular 2 CLI to an apache2-server with a PHP backend. Despite trying three different node modules, the issue persists from the OPTIONS-preflight stage, ...

Ordering a list of IP addresses using Angular Material table sorting

Here is an example I am baffled by the fact that Material Table sorting does not properly arrange the table. I have created a stackblitz example to demonstrate this. Expected order - Sorting lowest IP first: "10.16.0.8" "10.16.0.16" & ...

Selecting nested attributes from a class or interface: Best practices

I am looking to retrieve a specific type from the fn function. Let's take a look at the code snippet below: for more information, this is a continuation of a previous question on Stack Overflow: this question class Person { firstName: string; las ...

Retrieve a particular element from an array within a JSON object using Ionic

I am currently facing a challenge in extracting a specific array element from a JSON response that I have retrieved. While I can successfully fetch the entire feed, I am struggling to narrow it down to just one particular element. Here is what my service ...

The successful conversion of Typescript to a number is no longer effective

Just the other day, I was successfully converting strings to numbers with no issues. However, today things have taken a turn for the worse. Even after committing my changes thinking all was well, I now find that when attempting to cast in different ways, I ...

Encountering ORA-01008 error while utilizing nodeOracledb in TypeScript

I am facing an issue with the result of my code. I am trying to connect a Node.js script with Oracle using TypeScript, but for some reason, an error keeps appearing in my console. I have attempted various solutions to resolve this error, but unfortunately, ...

Utilizing TypeScript's conditional types in conjunction with enums

In my code, there is a TypeScript enum defined like this: enum UIConfigurationType { DisplayTableFields = "display-table-field", FormFields = "form-field", MainAttributes = "main-attribute", SearchAttributes = "se ...

Developing a FixedUpdate Function in TypeScript

Currently, I am delving into the world of game-engine development using TypeScript to expand my skills. The fixed update mechanism in Unity operates on a theoretical premise that involves processing physics within a while loop. Here's how it is envisi ...

Inoperative due to disability

Issue with Disabling Inputs: [disabled] = true [disabled] = "isDisabled" -----ts > ( isDisabled=true) Standard HTML disabler disable also not functioning properly [attr.disabled] = true [attr.disabled] = "isDisabled" -----ts > ( isDisabled=true) ...

Angular date selection with a range of plus two days, factoring in the exclusion of weekends

I am currently using a mat date picker range with specific logic. The minimum date that a user can select on the calendar is set to + 2 days. For example, if today's date is July 20, 2022, the minimum selectable date would be July 22, 2022. However, ...

Error in routing syntax, angular 2 zone symbol misconfiguration

Encountered an error while attempting to route my app: metadata_resolver.js:972 Uncaught SyntaxError {__zone_symbol__error: Error: Unexpected value '[object Object]' imported by the module 'AppModule' at SyntaxError.Zone……} app.c ...