Dealing with a reduction in a union type: pointers and advice

I am encountering an issue with the code snippet below: (TS Playground link)

type TDeliveriesStatsDatum<TName = string> = {name: TName; value: number};
type TDeliveriesStatsData<TName = string> = TDeliveriesStatsDatum<TName>[];

interface IDeliveriesStats {
    meta: {
        delivery_count: number;
        transport_count: number;
    };
    progress: TDeliveriesStatsData<"done" | "not_done" | "cancelled">;
    disputes: TDeliveriesStatsData<"disputed" | "undisputed">;
    loading_punctuality: TDeliveriesStatsData<"ontime" | "delayed" | "unknown">;
    unloading_punctuality: TDeliveriesStatsData<"ontime" | "delayed" | "unknown">;
    cmr_document: TDeliveriesStatsData<"done_with_cmr_document" | "done_without_cmr_document">;
    non_cmr_document: TDeliveriesStatsData<
        "done_with_non_cmr_document" | "done_without_non_cmr_document"
    >;

    loading_delay: TDeliveriesStatsData;
    unloading_delay: TDeliveriesStatsData;
    loading_duration: TDeliveriesStatsData;
    unloading_duration: TDeliveriesStatsData;
}

type DeliveriesStatsKeys = "progress" | "disputes" | "cmr_document" | "non_cmr_document";


type TPieChartData<T extends DeliveriesStatsKeys> = {
    augmentedData: {name: string, value: number, dataKey: string, fill: string}[] 
} & {
    [K in IDeliveriesStats[T][0]["name"]]: number;
};

export const formatPieChartData = <K extends DeliveriesStatsKeys>(
    data: IDeliveriesStats[K]
): TPieChartData<K> => {
    return data.reduce(
        (acc: TPieChartData<K>, datum: IDeliveriesStats[K][0]) => {

            acc[datum.name] = datum.value;

            acc.augmentedData.push({
                ...datum,
                dataKey: datum.name,
                fill: "colorsAndLabelsByDataKey[datum.name].fill,"
            });
            return acc;
        },
        {augmentedData: []} as TPieChartData<K>
    );
};

The error message from the compiler states:

This expression is not callable.
  Each member of the union type '{ (callbackfn: (previousValue: TDeliveriesStatsDatum<"done" | "not_done" | "cancelled">, 
currentValue: TDeliveriesStatsDatum<"done" | "not_done" | "cancelled"', 
currentIndex: number, array: TDeliveriesStatsDatum<...>[]) => TDeliveriesStatsDatum<...&...', 
has signatures, but none of those signatures are compatible with each other.

To resolve this issue, I need to properly type IDeliveriesStats or TPieChartData so that TypeScript understands that acc[datum.name] is correctly typed. How can I achieve this?

Answer №1

One of the issues here arises from the compiler's inability to establish a connection between the type of acc and the type of datum.name, unless these types are explicitly defined in a certain manner, as detailed in microsoft/TypeScript#47109. Without this explicit specification, the correlation is lost, leading the type checking process to treat acc and datum.name as separate entities constrained to unions, resulting in complaints about potential mismatches between source and target properties (as observed in microsoft/TypeScript#30769). While in reality such discrepancies might be unlikely, the compiler is unable to discern this information.

Another issue stems from the fact that the compiler interprets the data.reduce() call as an attempt to invoke a union of generic methods. This occurs because data belongs to a generic type that can only be recognized as an array through eager evaluation, thereby widening K to its constraint. The invoking of unions of generic methods is not fully supported; refer to microsoft/TypeScript#44373 for more insights. Once again, specifying the type of data in a particular manner helps the compiler understand that it pertains to a single array type with a singular generic reduce() method.


To address these concerns, a refactoring approach involves framing everything in terms of a generic key type allowing indexed access into a mapped type... typically derived from a fundamental "mapping" type structure. Here's how this transformation can be accomplished:

// basic mapping type
interface DeliveriesStatsName {
    progress: "done" | "not_done" | "cancelled"
    disputes: "disputed" | "undisputed"
    cmr_document: "done_with_cmr_document" | "done_without_cmr_document"
    non_cmr_document: "done_with_non_cmr_document" | "done_without_non_cmr_document"
}

type DeliveriesStats<K extends keyof DeliveriesStatsName> =
    DeliveriesStatsData<DeliveriesStatsName[K]>;

interface AugmentedData {
    augmentedData: {
        name: string, value: number, dataKey: string, fill: string;
    }[]
}

type BasePieChartData<K extends keyof DeliveriesStatsName> =
    Record<DeliveriesStatsName[K], number>;

type PieChartData<K extends keyof DeliveriesStatsName> =
    BasePieChartData<K> & AugmentedData;

In the above scenario, DeliveriesStats<K> mirrors your IDeliveriesStats[K], but presents an explicit representation linked to the mapping type DeliveriesStatsName. Similarly, PieChartData<K> serves a similar function to TPieChartData<K>, albeit with a specific mapping type depiction.

The intended outcome is for the compiler to recognize that data aligns with a distinct array type, where acc[datum.name] = datum.value indexes into Record<XXX, number> using a XXX typed key thus accepting a number.

While we approach close to achieving these objectives, complete success is elusive:

export const formatPieChartData = <K extends keyof DeliveriesStatsName>(
    data: DeliveriesStats<K>
): PieChartData<K> => {
    return data.reduce((acc, datum) => {

        acc[datum.name] = datum.value; // error!
        // Type 'number' is not assignable to type 'PieChartData<K>[DeliveriesStatsName[K]]'.

        acc.augmentedData.push({
            ...datum,
            dataKey: datum.name,
            fill: "colorsAndLabelsByDataKey[datum.name].fill,"
        });
        return acc;
    }, { augmentedData: [] } as AugmentedData as PieChartData<K>);
    // double assertion needed here to get the compiler to see the types as related

};

The ability to call data.reduce() has been achieved successfully, which is positive. However, errors persist when attempting acc[datum.name] = datum.value. This issue arises due to complications with intersections of generic record types. While there isn't an official report on this matter, a scenario like Record<K, V> & Foo where K is generic can confuse the compiler when indexing with a key K. As a workaround, transitioning Record<K, V> & Foo to just Record<K, V> before performing the index operation seems to resolve the problem. For additional details, refer to this answer.

Let's implement this fix:

export const formatPieChartData = <K extends keyof DeliveriesStatsName>(
    data: DeliveriesStats<K>
): PieChartData<K> => {
    return data.reduce((acc, datum) => {

        const _acc: BasePieChartData<K> = acc; // okay
        _acc[datum.name] = datum.value; // okay

        acc.augmentedData.push({
            ...datum,
            dataKey: datum.name,
            fill: "colorsAndLabelsByDataKey[datum.name].fill,"
        });
        return acc;
    }, { augmentedData: [] } as AugmentedData as PieChartData<K>);

};

The transition from acc to _acc receives approval from the compiler as safe, enabling successful assignment in _acc[datum.name] as intended!

Playground link to code

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

Is the Prisma model not reachable through Prisma Client?

I'm currently attempting to retrieve a specific property of a Prisma model using Prisma Client. The model in question is related to restaurants and includes a reviews property that also corresponds with a separate Review model. schema.prisma file: // ...

Is it possible to delay the execution of the onkeypress event by one second in order to prevent it from triggering a method multiple times consecutively?

Currently, I am developing a program that utilizes a method to display data in a table using a textbox. The issue is that the program is being called more than 10 times with each execution. Is there any way to prevent this from happening? The textbox elem ...

The problem encountered with the Enzyme mount API is a TypeError where it is unable to read the property 'find' of an undefined variable

After converting my component to a stateless one, I started encountering an error that says: TypeError: Cannot read property 'find' of undefined Previously, my tests were running smoothly before the switch. As far as I know, you can test functio ...

Interactive Bootstrap 4 button embedded within a sleek card component, complete with a dynamic click event

I am trying to create a basic card using bootstrap 4 with the following HTML code. My goal is to change the style of the card when it is clicked, but not when the buttons inside the card are clicked. Currently, clicking on the test1 button triggers both ...

Unraveling the secrets of Rxjs chaining

I have the following code that is functional and works as intended. canActivate(route: ActivatedRouteSnapshot): Observable<UrlTree | boolean> { return new Observable<UrlTree | boolean>((observer) => { this._authorizedStore .selec ...

I am experiencing issues with my HTML select list not functioning properly when utilizing a POST service

When using Angularjs to automatically populate a list with *ngFor and then implementing a POST service, the list stops functioning properly and only displays the default option. <select id="descripSel" (change)="selectDescrip()" > <option >S ...

When the key property is utilized, the state in react useState is automatically updated; however, for updating without it, the useEffect or a

I am working on a component that looks like this: import React, { useState } from "react"; import { FormControl, TextField } from "@material-ui/core"; interface IProps { text?: string; id: number; onValueChange: (text: stri ...

Accessing User Session with NextAuth in Application Router API Route

Utilizing NextAuth in conjunction with Next.js's App Router, I have established an API route within my /app/api directory. Despite my efforts, I am encountering difficulties retrieving the session associated with the incoming request. Below is the sn ...

Postponing a function invocation in an Angular/TypeScript application

I am facing a challenge in my Angular project. I have a form on one of my pages that allows users to submit data which is then sent to the database. However, after submitting the data, I need to delete the user's information from one table and insert ...

tips for populating input form with initial retrieved data

Hey there! I am currently working on fetching some data and my goal is to display these values as the initial input in a form. However, I am encountering an issue where instead of displaying the values, I am getting "undefined" even though I can see the da ...

Passing properties from the parent component to the child component in Vue3JS using TypeScript

Today marks my inaugural experience with VueJS, as we delve into a class project utilizing TypeScript. The task at hand is to transfer the attributes of the tabsData variable from the parent component (the view) to the child (the view component). Allow me ...

Display elements conditionally based on the result of an asynchronous operation within an ng

Using Angular version 11.0.2 I am encountering issues with the async pipe inside an ngIf template, where my data is not being properly refreshed. To illustrate this problem, I have created a simplified example. View code on Plunker To reproduce the issu ...

Issue: Unable to locate API compiler-cli, function __NGTOOLS_PRIVATE_API_2

I am currently working on an Angular project and encountered the following errors, $ ng serve Your global Angular CLI version (8.3.21) is higher than your local version (7.0.7). The local Angular CLI version will be used. To disable this warning, us ...

What are the best practices for utilizing generics effectively?

A series of interfaces has been defined: export interface FormData<T extends ControlData = any> { [type: string]: T; } export type FormResult<T extends FormData> = { [type in keyof T]: T[type]; }; export interface ControlData<T = any& ...

Issues with PrimeNG listbox functionality in Angular 2

In my reactive form, there are two controls. One control is a dropdown for selecting a country, and the other control is a list of checkboxes for selecting cities within the chosen country. The issue arises when I use primeNg's listbox for the city se ...

Exploring Tailwind's flexibility with custom color schemes

I'm attempting to generate unique hex colors for my React/TypeScript application with Tailwind. Why isn't the background color updating based on the color variable value? Check out my code snippet below: import React, { useState } from &apo ...

The element 'fontFamily' is not recognized within the 'ThemeOptions' type in MUI theming

I'm diving into the world of React and MUI by building my own dashboard from scratch. Let's take a look at my App.tsx file: import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; i ...

Placing options and a clickable element within a collapsible navigation bar

Within my angular project, there are 4 input fields where users need to enter information, along with a button labeled Set All which will populate them. https://i.sstatic.net/1GGh1.png I am wondering how I can organize these input fields and the button i ...

Having trouble accessing a downloaded image saved in local files from Amazon S3 using the AWS SDK in Node.js

I am currently using the node.js aws-sdk package to download files from s3 storage. However, when I download a jpeg image and save it as a local file, I am unable to view it. Is this the correct method for downloading jpeg images? public async downloadFi ...

Tips for preventing repetition in http subscribe blocks across various components

Imagine a scenario where there is a service used for making HTTP request calls. There are two different components (which could be more than two) that need to send the same request using the same observables via this service. After receiving the result, it ...