Is there a more efficient method to specify to typescript the type of "data"?

Recently, I've adopted the action/reducer pattern for React based on Kent Dodds' approach and now I'm exploring ways to enhance type safety within it.

export type Action = 
    { type: "DO_SOMETHING", data: { num: Number } } |
    { type: "DO_SOMETHING_ELSE", data: { nums: Number[] } };

type Actions = {
    [key in Action["type"]]: (state: State, data: Action["data"]) => State;
};

const actions: Actions = {
   DO_SOMETHING: (state, data) => {
       return { nums: [data.num] }; // Encountering a type error here
   },
   DO_SOMETHING_ELSE: (state, data) => {
       return { nums: data.nums }; // Also resulting in a type error
   }
};

The beauty of this code lies in its ability to guarantee that the actions object includes all the defined action types from the union type Action, while offering type safety when dispatching an action. The issue arises when trying to access members of data.

Property 'num' does not exist on type '{ num: Number; } | { nums: Number[]; }'.
  Property 'num' does not exist on type '{ nums: Number[]; }'.

However, by structuring the code as follows:

export type Action = 
    { type: "DO_SOMETHING", data: { num: Number } } |
    { type: "DO_SOMETHING_ELSE", data: { nums: Number[] } };

type Actions = {
    [key in Action["type"]]: (state: State, action: Action) => State;
};

const actions: Actions = {
   DO_SOMETHING: (state, action) => {
       if (action.type !== "DO_SOMETHING") return state;
       return { nums: [action.data.num] }; // Resolved the previous type error
   },
   DO_SOMETHING_ELSE: (state, action) => {
       if (action.type !== "DO_SOMETHING_ELSE") return state;
       return { nums: action.data.nums }; // No more type error encountered
   }
};

By adopting this structure, TypeScript is now able to recognize that action.data corresponds exactly with the specified action.type. Is there a more streamlined approach to achieve this without resorting to extensive inline actions within a switch statement?

PS - To delve deeper into these concepts, you may explore the comprehensive playground snippet I've been utilizing for testing purposes.

Answer №1

Almost there but not quite.

The issue lies in this code snippet Action['data'] within

(state: State, data: Action["data"]) => State;
, it should have been associated with the key property instead.

Action['data'] needs to be connected with the key property as shown in this sample:

type State = {
    nums: number[]
}
export type Action =
    | { type: "DO_SOMETHING", data: { num: number } }
    | { type: "DO_SOMETHING_ELSE", data: Pick<State, 'nums'> };

type Actions = {
    [Type in Action["type"]]: (state: State, data: Extract<Action, { type: Type }>['data']) => State;
};

const actions: Actions = {
    DO_SOMETHING: (state, data) => ({ nums: [data.num] }),
    DO_SOMETHING_ELSE: (state, data) => ({ nums: data.nums })
};

Check Playground

I opted for using Type in place of key since we are iterating through the types property.

The function Extract requires two arguments - a union as the first argument and the type it should match as the second. Think of it as similar to Array.prototype.filter for unions.

As a side note, please refrain from utilizing constructor types like Number; opt for number instead.

The interface Number pertains to number as an object whereas Number as a class corresponds to a class constructor:

interface Number {
    toString(radix?: number): string;
    toFixed(fractionDigits?: number): string;
    toExponential(fractionDigits?: number): string;
    toPrecision(precision?: number): string;
    valueOf(): number;
}

interface NumberConstructor {
    new(value?: any): Number;
    (value?: any): number;
    readonly prototype: Number;
    readonly MAX_VALUE: number;
    readonly MIN_VALUE: number;
    readonly NaN: number;
    readonly NEGATIVE_INFINITY: number;
    readonly POSITIVE_INFINITY: number;
}

declare var Number: NumberConstructor;

UPDATE

An excerpt from your shared example concerning the error you encountered:

function reducer(state: State, action: Action): State {
    
    /**
     * Argument of type '{ num: Number; } | { nums: Number[]; }'
     *  is not assignable to parameter of type '{ num: Number; } & { nums: Number[]; }'.
     */
    const newState = actions[action.type](state, action.data);
    return { ...state, ...newState };
}

This error occurs due to:

multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

Therefore, the second argument of actions[action.type] function represents an intersection of all arguments within the Actions list.

Here you can find more examples, and read my article here.

You can include a conditional statement like so:

const reducer = (state: State, action: Action): State => {
    if(action.type==="DO_SOMETHING"){
        const newState = actions[action.type](state, action.data); // ok
    }
// ....
}

However, this approach might not be optimal when dealing with numerous actions. Another method would be preferred.

This link provides a similar example for reference.

Hence, you have options aside from the previous ones.

Firstly, you can go for type assertion using as and proceed.

Alternatively, consider this approach:


type Builder<A extends { type: PropertyKey, data: any }> = {
    [K in A["type"]]: (state: State, data: Extract<A, { type: K }>["data"]) => State;
};


const reducer = <
    Type extends PropertyKey,
    Data,
    Act extends { type: Type, data: Data },
    Acts extends Builder<Act>
>(actions: Acts) =>
    (state: State, action: Act): State => {
        const newState = actions[action.type](state, action.data);
        return { ...state, ...newState };
    }

Notice how I inferred each property of Action and established a strong connection between actions and action.

Explore Full Example

P.S. Remember that reducer is curried, so ensure to pass it as reducer(actions) to useReducer.

Answer №2

const EventDataMap = {
    CLICK_EVENT: { count: Number },
    HOVER_EVENT: { counts: Number[] } 
};

const EventType = keyof EventDataMap
const EventsMap = {
  [E in EventType]: { event: E; info: EventDataMap[E] }
}

// creating a union type:
type Event = EventsMap[EventType]

// accessing specific events by indexing into EventsMap with the key variable
type EventHandlers = {
    [E in EventType]: (state: State, eventData: EventsMap[E]) => State;
};

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

Leveraging global attributes beyond Vue components - Vue 3

I have created custom service instances in main.ts app.config.globalProperties.$service1 = new Service1(); app.config.globalProperties.$service2 = new Service2(); While I can use these instances inside Vue components, I also want to be able to utilize the ...

Express not functioning properly with custom error handler

I'm facing an issue while trying to implement a custom error handler for my Express routes. Despite following the instructions in the documentation which recommend placing the custom handler at the end of the use chain, it seems that the default error ...

There was a TypeError error that occurred: Unable to access the property 'title' of null in Angular/Firebase

Seeking to retrieve data from Firebase's real-time database by passing values in an input form, I encountered an issue whenever I attempt to utilize [(ngModel)]="product.title". ERROR TypeError: Cannot read property 'title' of nu ...

Is it possible to use a '.JS' file downloaded through Node Package Manager (npm) directly in a web browser?

Generally, I am looking to utilize a specific library without relying on Node CMD. For instance: I aim to create a TypeScript playground without having to execute 'tsc.cmd' from "npm\node_modules", instead, I want to directly call the tsc c ...

Create a functioning implementation for retrieving a list of objects from a REST API

I am looking to incorporate an Angular example that retrieves a list from a REST API. Here is what I have attempted: SQL query: @Override public Iterable<Merchants> findAll() { String hql = "select e from " + Merchants.class.getName ...

"Looking to personalize marker clusters using ngx-leaflet.markercluster? Let's explore some ways to customize

I am currently struggling to implement custom cluster options in ngx-leaflet. My goal is simply to change all marker clusters to display the word "hello". The demo available at https://github.com/Asymmetrik/ngx-leaflet-markercluster/tree/master/src/demo/a ...

Retrieving a variable value set within a jQuery function from within an Angular 2 component

In the current project, I am facing a situation where I need to work around and initialize jQuery datetimepicker inside an Angular 2 application (with plans to refactor it later). However, when I assign a datetime value to a variable, I encounter a proble ...

Using RxJs in Angular, you can invoke an observable from within an error callback

I have a scenario where two observable calls are dependent on each other. Everything works fine, but when an error occurs in the response, I need to trigger another observable to rollback the transaction. Below is my code: return this.myService.createOrde ...

Pause and be patient while in the function that delivers an observable

I have a function that loads user details and returns an observable. This function is called from multiple places, but I want to prevent redundant calls by waiting until the result is loaded after the first call. Can anyone suggest how this can be accompli ...

Encountering a compiler error due to lack of patience for a promise?

In the current TypeScript environment, I am able to write code like this: async function getSomething():Promise<Something> { // ... } And later in my code: const myObject = getSomething(); However, when I attempt to use myObject at a later po ...

Unselected default option in Angular 4's select dropdown

My goal is to use Angular to retrieve a value from a variable and display it as the first option in a select element, while keeping the rest of the options below static. The issue I am facing is that even though Angular is fetching the data successfully, t ...

Empowering your Angular2 application with data binding

I am currently working with the following template: <table width="700"> <caption>All Users</caption> <thead> <tr> <th>name</th> <th>surname</th> < ...

The module './product-list/product-list.component' could not be located

As a beginner in Angular, I decided to incorporate a top-bar component into my project by following the structure outlined in the app directory. However, I encountered some issues as indicated by the error message that kept popping up. Despite importing ...

Differences Between Public and Private Constructors in TypeScript

Is it correct to say that public members within a TypeScript constructor are accessible throughout the class, while private members are restricted? If this is true, what sets public members apart from properties? If properties can behave more like c# pro ...

My router-outlet is malfunctioning when trying to display my component

I am currently diving into learning Angular 2 in order to revamp my personal website. However, I've encountered an issue where my application fails to load the component when I navigate to the appropriate route by clicking on the navigation bar. Insi ...

Utilizing the useSelect hook in Typescript to create custom types for WordPress Gutenberg, specifically targeting the core/editor

As I delve into development with WordPress and the Gutenberg editor, my goal is to incorporate TypeScript into the mix. However, I encounter a type error when trying to utilize the useSelect() hook in conjunction with an associated function from the core/e ...

Obtaining the dimensions of each individual child component within an NgTemplate

I have the following code snippet within my template. While I can iterate through its components using `get`, it does not return an object that allows me to access deeper into the HTML attributes. <ng-template #container></ng-template> Compon ...

Data has not been loaded into the Angular NGX Datatable

As a beginner in Angular, I am struggling to set data from the module. ngOnInit() { this.populate(); } public populate() { this.productService.getAllProduct('6f453f89-274d-4462-9e4b-c42ae60344e4').subscribe(prod => { this. ...

The defineProps<SomePropType>() method is not rendering the props as expected

I have various components, with a parent element where I attempted to pass props using the syntax defineProps<{}>(). The setup is simple, I have a types.ts file at the root level, a parent Vue file (referred to as CardItem), and multiple components. ...

Unraveling URLs in JSON with TypeScript

After retrieving a Json string from the BE API, I am parsing it into an array of Products[]. This collection is structured as follows: class Products { ProductId: number; ProductName: string; Price: number; ProductUrl: string; } The issue I' ...