Mapping two objects of the same shape to each other recursively using TypeScript

I receive regular data from a specific source. This data consists of nested objects containing numerical values. For example:

{
 a: 1,
 b: {
  c: 2,
  d: 3.1,
 },
}

My objective is to organize this data into multiple TimeSeries objects of the same structure:

const timeSeries = {
 a: new TimeSeries(),
 b: {
  c: new TimeSeries(),
  d: new TimeSeries(),
 },
}

Upon each 'update' event, I need to invoke TimeSeries.append(time, data) for every new data point:

timeSeries.a.append(time, data.a);
timeSeries.b.c.append(time, data.b.c);
timeSeries.b.d.append(time, data.b.d);

The ultimate goal is to automate this process as the structure of the incoming data evolves.

Currently, I have implemented the JavaScript functionality but struggle with ensuring proper typing in TypeScript. Any suggestions?


// The structure of the data is known during compilation.
// A manual instance initialization is performed elsewhere, which I aim to avoid duplicating for TimeSeries objects.
type Data = {
 a: number;
 b: {
  c: number;
  d: number;
 }
}

// Definition of the TimeSeries interface used for replication.
interface TimeSeries {
 append(time: number, value: number): void;
}

// Utility function
function isNestedValues<V, T extends {}>(value: V | T): value is T {
  return typeof value === 'object';
}

// Successful type mapping
type MappedTimeSeries<T extends {}> = {
  [P in keyof T]: T[P] extends {} ? MappedTimeSeries<T[P]> : TimeSeries;
};

// Mapping of time series.
// Initially partial until filled with data.
// Preventing premature usage of TimeSeries objects before they are ready.
export const timeSeries = {} as Partial<MappedTimeSeries<Data>>;

// Enhancing strict typing for numeric values would be beneficial
function updateTimeSeriesRecursive<T extends {}>(time: number, o: T, ts: MappedTimeSeries<T>) {
  for (const x in o) {
    type P = Extract<keyof T, string>;
    if (isNestedValues(o[x])) {
      // Initialization of TimeSeries{} upon receiving the first set of data
      if (!ts[x]) ts[x] = {} as MappedTimeSeries<typeof o[x]>;

      updateTimeSeriesRecursive(time, o[x], ts[x]);
    } else {
      // Initializing actual TimeSeries object upon receiving the first set of data
      if (!ts[x]) ts[x] = new TimeSeries();

      (ts[x] as TimeSeries).append(time, o[x]);
    }
  }
}

source.on('update', time: number, (data: Data) => {
  updateTimeSeriesRecursive(time, state, timeSeries);
});

Answer №1

In my view, there isn't a flawless solution because some of the type manipulations you're working on are beyond the compiler's capabilities to follow. This necessitates the use of type assertions or similar techniques (like single call-signature overloaded functions). Here's an approach I would take:

type DataConstraint<T> = { [K in keyof T]: DataConstraint<T[K]> | number }

type MappedTimeSeries<T extends DataConstraint<T>> = {
    [K in keyof T]?: T[K] extends DataConstraint<T[K]> ? MappedTimeSeries<T[K]> : TimeSeries }

const timeSeries: MappedTimeSeries<Data> = {};

function updateTimeSeriesRecursive<T extends DataConstraint<T>>(
    time: number, o: T, ts: MappedTimeSeries<T>
): void;
function updateTimeSeriesRecursive(
    time: number, o: DataConstraint<any>, ts: MappedTimeSeries<any>
): void {
    for (const x in o) {
        const ox = o[x];
        if (typeof ox === "number") {
            const tsx = ts[x] = ts[x] || new TimeSeries();
            (tsx as TimeSeries).append(time, ox);
        } else {
            const tsx = ts[x] = ts[x] || {};
            updateTimeSeriesRecursive(time, ox, tsx as MappedTimeSeries<DataConstraint<any>>);
        }
    }
}

That's pretty close to what you have already. Notable changes include:

  • The DataConstraint<T> type represents the constraint that the properties of a Data-like type should be either number or other DataConstraint objects.

  • You can constrain MappedTimeSeries<T> so that T must conform to DataConstraint<T>. The conditional type check in your original definition was improved by using

    T[K] extends DataConstraint<T[K]> ? ... 
    .

  • The updateTimeSeriesRecursive() function has a single call-signature overload which is generic and an implementation signature which uses wider types like DataConstraint<any> and MappedTimeSeries<any>.

There's more to it, but these are the key elements. It works when implementing a function like this:

function doStuff(time: number, data: Data) {
    updateTimeSeriesRecursive(time, data, timeSeries); // works fine
}

doStuff(1, { a: 1, b: { c: 2, d: 3 } });
doStuff(2, { a: 2, b: { c: 4, d: 4 } });
doStuff(3, { a: 3, b: { c: 6, d: 5 } });
doStuff(4, { a: 4, b: { c: 8, d: 6 } });

Link to code with example

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

Tips for splitting JSON objects into individual arrays in React

I'm currently tackling a project that requires me to extract 2 JSON objects into separate arrays for use within the application. I want this process to be dynamic, as there may be varying numbers of objects inside the JSON array in the future - potent ...

Steps for clicking on the center of a leaflet map with protractor

I'm currently facing an issue where I am attempting to click on the center of a map located in the second column of the webpage, which has an offset. However, I am encountering a problem where the cursor always points to the center of the page instead ...

deliver a promise with a function's value

I have created a function to convert a file to base64 for displaying the file. ConvertFileToAddress(event): string { let localAddress: any; const reader = new FileReader(); reader.readAsDataURL(event.target['files'][0]); reader ...

Setting up an inline style @Input in Angular 2: A step-by-step guide

I am currently working on a component that needs to display random values, which will be generated randomly and passed through some @Input bindings in the template. Everything seems to be going well, but I am facing an issue when trying to link an @Input t ...

After integrating React Query into my project, all my content vanishes mysteriously

Currently, I am utilizing TypeScript and React in my project with the goal of fetching data from an API. To achieve this, I decided to incorporate React Query into the mix. import "./App.css"; import Nav from "./components/Navbar"; impo ...

Enhancing supertest functionality with Typescript

Currently, I am working on extending the functionality of supertest. After referencing a solution from Extending SuperTest, I was able to implement the following example using javascript: const request = require('supertest'); const Test = reque ...

Steer clear of using the non-null assertion operator while assigning object members

Looking for a practical method to assign object members to another object without using the non-null assertion operator "!". In the example below, consider that formData can be any JavaScript object. some.component.ts export class SomeComponent { someMo ...

Can you identify the nature of the argument(s) used in a styled-component?

Utilizing typescript and react in this scenario. Fetching my variable const style = 'display: inline-block;' Constructing a simple component export const GitHubIcon = () => <i className="fa-brands fa-github"></i> Enh ...

Troubleshooting TypeScript errors in a personalized Material UI 5 theme

In our codebase, we utilize a palette.ts file to store all color properties within the palette variable. This file is then imported into themeProvider.tsx and used accordingly. However, we are encountering a typescript error related to custom properties as ...

Serialising and deserialising TypeScript types in local storage

I'm currently working on a Typescript application where I store objects using local storage for development purposes. However, I've run into some trouble with deserialization. Specifically, I have an object called meeting of type MeetingModel: ...

I am eager to learn how to integrate the "fs" module from Node.js into an Electron project powered by Angular

As I venture into writing my first desktop app using Electron and Angular5, I have encountered a roadblock while working with the fs module. Despite importing fs correctly (without errors in Visual Studio Code and with code completion), I faced an issue wh ...

"Encountered a problem when trying to import stellar-sdk into an Angular

Our team is currently working on developing an app that will interact with the Horizon Stellar Server. As newcomers in this area, we are exploring the use of Angular 8 and Ionic 4 frameworks. However, we have encountered difficulties when trying to import ...

Creating a dynamic array in an Angular 2 service for real-time changes monitoring by a component

I am facing an issue where my NotificationsBellComponent is not receiving changes to the array in the service even though the _LocalStorageService is returning data correctly. Q) How can I ensure that my component receives updates when the service collect ...

Access values of keys in an array of objects using TypeScript during array initialization

In my TypeScript code, I am initializing an array of objects. I need to retrieve the id parameter of a specific object that I am initializing. vacancies: Array<Vacancy> = [{ id: 1, is_fav: this.favouritesService.favourites.find(fav = ...

Put an end to the endless game of defining TypeScript between Aurelia CLI and Visual Studio 2017 builds

I am encountering TypeScript errors in my Visual Studio build for an Aurelia project within a .NET Core project. The errors include 'Build:Cannot find name 'RequestInit'', 'Build:Cannot find name 'Request'', and &apo ...

The 'style' property is not found within the 'EventTarget' type

Currently, I am utilizing Vue and TypeScript in an attempt to adjust the style of an element. let changeStyle = (event: MouseEvent) => { if (event.target) { event.target.style.opacity = 1; Although the code is functional, TypeScript consist ...

An action in redux-toolkit has detected the presence of a non-serializable value

When I download a file, I store it in the payload of the action in the store as a File type. This file will then undergo verification in the saga. const form = new FormData(); if (privateKey && privateKey instanceof Blob) { const blob = new Blo ...

Attempting to retrieve data either by code or with a WHERE condition proves unsuccessful as the data retrieval process yields no results

Seeking assistance with my Angular project that is utilizing a Node.js server and MSSQL express. I am having trouble retrieving data using a WHERE condition in my code. Any help in identifying the missing piece or error would be appreciated. Thank you. // ...

Positioning customized data on the doughnut chart within chart.js

Is there a way to customize the position of the data displayed in a doughnut chart? Currently, the default setting is that the first item in the data array is placed at 0 degrees. However, I need to place it at a custom position because I am working on a ...

Running a Vue.js 3 application with TypeScript and Vite using docker: A step-by-step guide

I am currently facing challenges dockerizing a Vue.js 3 application using Vite and TypeScript. Below is my Dockerfile: FROM node:18.12.1-alpine3.16 AS build-stage WORKDIR /app COPY package.json ./ RUN yarn install COPY . . RUN yarn build-only FROM ngin ...