Saving type while mutating accumulator in Array.reduce in Typescript

This particular function is designed to add a deeply nested property to an object by taking a string argument in the format of 'a.very.deep.property'.

function nest<B extends obj, V = unknown>(
    target: B,
    structure: string,
    value: V,
) {
    const properties = structure.split('.');
    const result = target;
    properties.reduce((acc, property, i, arr) => {
        const isLastProperty = i === arr.length - 1;
        if (!(property in acc))
            acc[property] = isLastProperty ? value : {};
        return acc[property];
    }, target);
    return target;
}

While this function works well in JavaScript, it encounters an error in TypeScript where

Type 'string' cannot be used to index type 'B'
when attempting to assign accum[property]. One could typically avoid mutating acc by creating another object with intersection type, however using reduce implies that mutation of acc inside the callback is necessary to obtain the final result.

(accum as B & { [property: string]: obj | V })[property] = isLastProperty ? value : {};
also does not solve the issue, resulting in the error
type 'string' cannot be used to index type 'B & { [property: string]: obj | V;
. What would be the best approach in this situation?

Answer №1

Trying to achieve this in TypeScript may pose some challenges due to its static typing nature, while the resulting array of split properties is dynamic and often only known at runtime.

Despite this, it can still be done with a more verbose approach:

function nest<B extends object, V = unknown>(
    target: B,
    structure: string,
    value: V,
) {
    const properties = structure.split('.');
    const result = target;
    const lastProp = properties.pop();
    const lastObj = properties.reduce((acc, property) => {
        if (!(property in acc))
            acc[property] = {};
        const nestedVal = acc[property];
        if (!nestedVal || typeof nestedVal !== 'object') {
            throw new Error();
        }
        return nestedVal;
    }, target);
    lastObj[lastProp] = value;
    return target;
}

The key aspects to consider are:

  • Appending the final nested value after the reduce loop to ensure proper typing within the callback function
  • Throwing an error if the property does not exist or is not an object to enforce type safety
  • Storing acc[property] in a separate variable for better type narrowing as direct narrowing using in might not work effectively

Answer №2

It was quite a challenge, but after utilizing Typescript 4 template literals I successfully created a type-safe version of this solution.

    type DeepType<T, S extends string> = T extends object
            ? S extends `${infer Key}.${infer NextKey}`
                ? Key extends keyof T
                    ? DeepType<T[Key], NextKey>
                    : false
                : S extends keyof T
                ? T[S]
                : never
            : T;
        
    type RecursiveKeyOf<TObj extends object> = {
            [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
                TObj[TKey],
                `${TKey}`
            >;
        }[keyof TObj & (string | number)];
        type RecursiveKeyOfInner<TObj extends object> = {
            [TKey in keyof TObj & (string | number)]: RecursiveKeyOfHandleValue<
                TObj[TKey],
                RecursiveKeyOfAccess<TKey>
            >;
        }[keyof TObj & (string | number)];
        type RecursiveKeyOfHandleValue<
            TValue,
            Text extends string
        > = TValue extends object
            ? Text | `${Text}${RecursiveKeyOfInner<TValue>}`
            : Text;
        type RecursiveKeyOfAccess<TKey extends string | number> =
            | `['${TKey}']`
            | `.${TKey}`;
type obj = Record<string, any>;

export function addNestedProperty<
    B extends Record<string, any>,
    P extends string & RecursiveKeyOf<B>,
    V extends DeepType<B, P>
>(
    base: B,
    path: P,
    value: V,
    o: { overwrite: boolean } = { overwrite: true },
): B {
    const properties = path.split('.') as Array<string & keyof B>;
    const lastProperty = properties.pop();
    if (!lastProperty)
        throw new Error('path argument must contain at least one property');

    function isObject(arg: unknown): arg is object {
        return typeof arg === 'object' && arg !== null;
    }

    const lastObj = properties.reduce((acc, property) => {
        if (!(property in acc)) acc[property] = {} as any;
        else if (!isObject(acc) && !o.overwrite)
            throw new Error('you are trying to overwrite existing property');
        return acc[property];
    }, base);

    lastObj[lastProperty] = value;
    return base;
}

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

Struggling to retrieve the Object from a JSON file located at a specific URL

Apologies if this comes across as naive, but my venture into javascript and json is just starting. I'm eager to access the JSON object from the twitter API after executing var myjson; $.getJSON('url of the Object', function(data) { ...

Creating a customized HTTP class for Bootstrap in Angular 2 RC 5

During my experience with Angular 2 RC 4, I encountered a situation where I needed to create a class called HttpLoading that extended the original Http class of Angular2. I managed to integrate this successfully into my project using the following bootstr ...

Encoding the img src is necessary when assigning it to an SVG element

I am encountering an issue with converting an SVG element generated by D3 into a PNG using dom-to-image within my Angular 2 application. The problem arises when I attempt to set the SVG as the source of an image element, resulting in the SVG code being enc ...

Error causing expression change after Angular binding has been checked

Take a look at this demo: @Component({ selector: 'my-app', template: ` <div> <h1>{{ foo }}</h1> <bpp [(foo)]="foo"></bpp> </div> `, }) export class App { foo; } @Component({ ...

Migration of old AngularJS to TypeScript in require.js does not recognize import statements

I am looking to transition my aging AngularJS application from JavaScript to TypeScript. To load the necessary components, I am currently utilizing require.js. In order to maintain compatibility with scripts that do not use require.js, I have opted for usi ...

Issues have arisen with the ReactNative Jest snapshot test, resulting in a

I decided to incorporate TypeScript into my react-native project. I came across this helpful article and followed the instructions step by step. However, when I ran yarn test, I encountered an error that I'm unsure how to resolve: FAIL Components/__ ...

An issue has been encountered within the node modules directory at the path node_modules/@angular/flex-layout/extended/typings

I encountered an error in my Angular 6.0.8 application while using Angular CLI and running from VSCode. ERROR in node_modules/@angular/flex-layout/extended/typings/style/style.d.ts(72,67): error TS1144: '{' or '; ' expected. no ...

Exploring the concept of using a single route with multiple DTOs in NestJS

At the moment, I am utilizing NestJS for creating a restful API. However, I am currently facing an issue with the ValidationPipe. It seems to only be functioning properly within controller methods and not when used in service methods. My goal is to implem ...

Animating Chart.js inside an Angular mat-tab component

I have a situation where I'm displaying multiple charts within a mat-tab, but I'm experiencing an issue with the animation of data in the chart. animation: { duration: 1000, easing: 'easeOutQuart' } The a ...

`AngularJS Voice Recognition Solutions`

In my quest to implement voice recognition in an AngularJS application I'm developing for Android and Electron, I've encountered some challenges. While I've already discovered a suitable solution for Android using ng-speech-recognition, fin ...

What could be the reason for my npm package installed globally to not be able to utilize ts-node?

Recently, I've been working on developing a CLI tool for my personal use. This tool essentially parses the standard output generated by hcitool, which provides information about nearby bluetooth devices. If you're interested in checking out the ...

Transform Text into Numeric Value/Date or Null if Text is Invalid

Consider the TypeScript interface below: export interface Model { numberValue: number; dateValue: Date; } I have initialized instances of this interface by setting the properties to empty strings: let model1: Model = { numberValue: +'', ...

No routes found to match - Issue encountered in Ionic 5 Angular project

I have a total of 15 pages within my project and I am looking to incorporate a page with 2 tabs. To make this happen, I have created a folder labeled tabs inside the existing app directory. Inside the tabs folder, there are 3 specific pages - 1. project v ...

How can I duplicate an array of objects in javascript?

I'm struggling with a javascript issue that may be due to my lack of experience in the language, but I haven't been able to find a solution yet. The problem is that I need to create a copy array of an array of objects, modify the data in the cop ...

What is the best way to execute 2 statements concurrently in Angular 7?

My goal is to add a key rating inside the listing object. However, I am facing an issue where the rating key is not displaying on the console. I suspect that it might be due to the asynchronous nature of the call. Can someone help me identify what mistak ...

Is there a way in Typescript to dynamically create a type based on an array of potential values?

I am seeking a solution to dynamically define a type based on an array of possibilities. Within the provided map, the keys represent the type's name, while the corresponding values are arrays containing possible options for that type. export const ty ...

Angular 11 causing UI flicker upon array modifications

When trying to access an updated array value from the server, I noticed that the UI template is flickering when concatenation occurs. How can this issue be resolved? @Input('companies') set setCompanyArray(companies) { this.showNotFound = fa ...

Ways to convert all keys to uppercase in an array of objects?

Is there a way to capitalize the first letter of every key in an array of objects? I attempted to achieve this with the code below, but it's not working as expected. Any suggestions or corrections are appreciated. #current code function capitalizeO ...

The declaration file for the 'express' module could not be located

Whenever I run my code to search for a request and response from an express server, I encounter an issue where it cannot find declarations for the 'express' module. The error message transitions from Could not find a declaration file for module & ...

Troubleshooting Problem with GraphQL and TypeORM: Retrieving Products Along with Their Images

Currently, I am developing a GraphQL API that utilizes TypeORM as the ORM to communicate with my PostgreSQL database. One of the challenges I am facing involves fetching products along with their corresponding images through GraphQL queries. In this scena ...