Tips for implementing a secure and efficient _.update function from lodash?

There is a handy feature in Lodash called update, which allows for easy object manipulation:

const obj = {}

_.update(obj, 'a.b.c', prev => prev ? 1 : 0)

console.log(obj) // { a: { b: { c: 0 } } }

Check it out on CodeSandbox →

By providing a path (as a string) as the second argument, the update function recursively creates the path and sets the value of the last item based on the updater's return value.

Unfortunately, there are limitations with typing support:

const obj = { a: { b: { c: "hello world" } } };

_.update(obj, "a.b.c", (prev) => (prev ? 1 : 0));

console.log(obj); // { a: { b: { c: 0 } } }

See on CodeSandbox →

The type of prev is identified as any, posing potential risks. This led me to explore creating my own alternative to Lodash's update utility after encountering difficulties in typing paths. I came across an answer that provided insight but found it overwhelming, especially when dealing with computed keys.

This prompted the creation of a custom wrapper for _.update:

export function mutate<T extends object, P extends object = object>(
  obj: T,
  path: P,
  updater: (prev: ValueOf<T>) => ValueOf<T>
): T {
  const actualPath = Object.keys(path)
    .map((o) => path[o as keyof P])
    .join(".");

  return _.update(obj, actualPath, updater);
}

const obj = { a: { b: { c: 123 } } };

const x = "a";
const y = "b";
const z = "c";

mutate(obj, { x, y, z }, (prev) => 123);

See on CodeSandbox →

While some progress has been made with mutate catching type mismatches, recursion remains a challenge leaving me puzzled about next steps.

Queries

  1. Is there a way to determine the type of a computed property like illustrated above?
  2. If yes, how can this be achieved?
  3. How can recursion be implemented, if feasible?
  4. If you're familiar with any other tool or npm package addressing this issue of object updates, please share your suggestions.

Answer №1

Let's start by focusing on ensuring the type safety of prev.

The goal is to receive a path P in the format of "a.b.c", and use this path to navigate through the object type T to obtain the type of a.b.c. This can be achieved using template literal types.

type GetTypeAtPath<T, P extends string> = 
  P extends `${infer L}.${infer R}`
    ? GetTypeAtPath<T[L & keyof T], R>
    : P extends keyof T 
      ? T[P]
      : undefined

The GetTypeAtPath type first attempts to split the string P into two string literal types L and R. If successful, it recursively calls itself with T[L & keyof T] as the new object type and R as the new path.

If the string cannot be split further because we have reached the last element, we check if P is a key in T. If it is, we return T[K]; otherwise, we return undefined.

This ensures the correct typing of prev.

const ret1 = mutate(obj, "a.b.c", (prev) => "123")
//                                 ^? number

const ret2 = mutate(obj, "a.b.newProperty", (prev) => "123")
//                                           ^? undefined

It is important to note that while this method works well for simple paths like "a.b.c", lodash functions support more complex path syntaxes such as "a.b[0].c" or "a.b.0.c", which may not be covered by this implementation. Additionally, there are various edge cases to consider, such as passing objects with index signatures or using the any type.


In addition, I have also defined a return type for the function without going into much detail.

function mutate<
  T,
  P extends string, 
  N
>(
  obj: T, 
  path: P, 
  updater: (prev: GetTypeAtPath<T, P>) => N 
): ExpandRecursively<ChangeType<T, P, N> & ConstructType<P, N>> {
    return null!
}

The return type of updater is used to infer R. We then modify the type of

T</code based on whether the property already exists or needs to be added using <code>ChangeType
and ConstructType.

type ChangeType<T, P extends string, N> = 
  {
    [K in keyof T]: P extends `${infer L}.${infer R}`
      ? L extends K 
        ? ChangeType<T[K], R, N>
        : T[K]
      : P extends K
        ? N
        : T[K]
  }

type ConstructType<P extends string, N> = 
  P extends `${infer L}.${infer R}`
    ? {
      [K in L]: ConstructType<R, N>
    } 
    : {
      [K in P]: N
    }

The final return type is an intersection of both computed types.


Playground

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

The function, stored as state within a context provider, is experiencing only one update

I am facing an issue with my Next.js and Typescript setup, but I believe the problem is more general and related to React. Despite extensive research on Stack Overflow, I have not come across a similar problem. To provide some context: I have a <Grid&g ...

Is it possible to measure the CPU utilization in a TypeScript application programmatically?

Is there a method to calculate CPU usage as a percentage and record it in a file every 20 milliseconds? I'm interested in exploring different approaches for accomplishing this task. Your insights would be greatly appreciated! I've come across so ...

The Angular 4 HTTP patch method is encountering difficulties when called in code but functions properly when tested using Post

My attempts to make a patch and post call to the server are failing as it never reaches the server. Interestingly, the same request works flawlessly in Postman, so I suspect there might be an issue with my code. Both my post and patch methods are essentia ...

angular2 and ionic2 encounter issues when handling requests with observable and promises

I am attempting to trigger an action once a promise request has been resolved, but I'm having trouble figuring out how to achieve this. After doing some research, I learned that Ionic2 storage.get() returns a promise, and I would like to make an HTTP ...

"Facing an issue where the import of React with Typescript and jQuery is failing to work

Currently, I am working with a Bootstrap plugin known as Bootstrap Toggle. I managed to download the necessary CSS and JS scripts via npm, however, jQuery is required for it to function. To address this requirement, I proceeded to download jQuery along wi ...

The shape of an array morphs into a nested structure after being processed with Map() during code compilation

When working on my application, I utilize the Map() function to eliminate duplicate items from an array: const uniqueTree = [ ...new Map( [newItem, ...oldData] ).values(), ] During development, this code snippet generates a flat array like so: [ ...

Streamlined Authorization in MEAN (SPA) Applications

I have created an application, but now I am trying to adapt it into a SPA application. The main issue I am facing is with the Authorization process. While I can successfully register new users, log them in, and retrieve their tokens, I seem to encounter a ...

Creating an array of objects using Constructors in Typescript

Utilizing TypeScript for coding in Angular2, I am dealing with this object: export class Vehicle{ name: String; door: { position: String; id: Number; }; } To initialize the object, I have followed these steps: constructor() { ...

A universal TypeScript type for functions that return other functions, where the ReturnType is based on the returned function's ReturnType

Greetings to all TypeScript-3-Gurus out there! I am in need of assistance in defining a generic type GuruMagic<T> that functions as follows: T represents a function that returns another function, such as this example: fetchUser(id: Id) => (disp ...

Exploring the Factory Design Pattern Together with Dependency Injection in Angular

I'm currently implementing the factory design pattern in Angular, but I feel like I might be missing something or perhaps there's a more efficient approach. My current setup involves a factory that returns a specific car class based on user input ...

The issue of binding subjects in an Angular share service

I established a shared service with the following Subject: totalCostSource$ = new Subject<number>(); shareCost(cost: number ) { this.totalCostSource$.next(cost); } Within my component, I have the following code: private incomeTax: num ...

The data type 'void | Observable<any>' cannot be assigned to the type 'ObservableInput<any>'. Specifically, the type 'void' cannot be assigned to 'ObservableInput<any>'

I encountered an error in my visual studio code: Argument of type '(query: string) => void | Observable' is not assignable to parameter of type '(value: string, index: number) => ObservableInput'. Type 'void | Observable& ...

What is the best way to restrict a mapped type in typescript to only allow string keys?

In the Typescript documentation, I learned about creating a mapped type to restrict keys to those of a specific type: type OptionsFlags<Type> = { [K in keyof Type]: boolean; }; If I want to use a generic type that only accepts strings as values: t ...

Utilize Typescript to ensure uniformity in object structure across two choices

Looking to create a tab component that can display tabs either with icons or plain text. Instead of passing in the variant, I am considering using Typescript to verify if any of the icons have an attribute called iconName. If one icon has it, then all othe ...

Setting up the parameters for functions that encapsulate specified functions

Currently, I am in the process of creating a simple memoize function. This function is designed to take another function and perform some behind-the-scenes magic by caching its return value. The catch is that the types for the returned function from memoiz ...

Guide for releasing a typescript package compatible with both node and react applications

I have developed an authentication library in TypeScript which I have released on npm. My goal is to make this library compatible for use in both Node.js projects and React projects created with create-react-app. However, I am facing an issue where one of ...

The Cypress command has gone beyond the specified timeout of '8710 milliseconds'

I ran a test in Cypress that initially passed but then failed after 8 seconds with the following error: "Cypress command timeout of '8710ms' exceeded." Console log Cypress Warning: It seems like you returned a promise in a test and also use ...

Enhance the Component Props definition of TypeScript 2.5.2 by creating a separate definition file for it

I recently downloaded a NPM package known as react-bootstrap-table along with its type definitions. Here is the link to react-bootstrap-table on NPM And here is the link to the type definitions However, I encountered an issue where the types are outdate ...

Executing a function after another one has completed in Angular 6

I need to run 2 methods in the ngOnInit() lifecycle hook, with method2() being executed only after method1() has completed. These methods are not performing HTTP requests and both belong to the same component. ngOnInit() { this.method1(); this. ...

Acquiring information and utilizing the useState hook

When working with a component that needs to retrieve data from a backend using a gql query, I've encountered a scenario where I'm using a state object to store the fetched values. The component's rendering logic is based on whether these key ...