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

What is the proper way to utilize the transform method in require('typescript')?

const babel = require('babel'); let sample = ` let greeting: string = 'hello, there'; ` babel.transform What is the method for changing strings within the provided code? ...

The React component continuously refreshes whenever the screen is resized or a different tab is opened

I've encountered a bizarre issue on my portfolio site where a diagonal circle is generated every few seconds. The problem arises when I minimize the window or switch tabs, and upon returning, multiple circles populate the screen simultaneously. This b ...

How to locate the position of an element within a multi-dimensional array using TypeScript

My data structure is an array that looks like this: const myArray: number[][] = [[1,2,3],[4,5,6]] I am trying to find the index of a specific element within this multidimensional array. Typically with a 1D array, I would use [1,2,3].indexOf(1) which would ...

Issue: Undefined default value property in TypescriptDescription: In Typescript,

export class EntityVM implements EntityModel { ... Properties... ... constructor(newEntity: EntityModel, isCollapsed = false) { ... Properties... ... this.isCollapsed = isCollapsed; } } public myFunction(myEntity: EntityVM) { / ...

Creating a Custom FlatList Content Container with React Native

Is it possible to customize FlatList items with a custom component? I want to create a setup where my FlatList items are encapsulated within a custom component similar to the following: <ScrollView pt={8} px={16} pb={128} > <Card e ...

Ways to observe redux action flow within a component

I am currently developing a React application structured with the following elements: redux typesafe-actions redux-observable My query is: How can I trigger a UI action when a specific redux action occurs? For instance, if we have these asynchronous ac ...

Angular 8 Refresh Token Implementation

I am currently working on an Angular 8 app that is integrated with .NET Core. My goal is to find a way to refresh a JWT token within the application. Within the integration, users are able to validate and receive a token which expires after 30 minutes. T ...

Minimize the quantity of data points displayed along the X-axis in a highcharts graph

After making an API call, I received data for a Highcharts graph consisting of an array of datetimes (in milliseconds) and corresponding values (yAxis). The data is fetched every 15 minutes and displayed on a mobile device. When viewing the data monthly, ...

Error message encountered: Missing property status in TypeScript code

An error occurs in the refetchInterval when accessing data.status, with a message saying "property status does not exist" chatwrapper.tsx const ChatWrapper = ({ fileId }: ChatWrapperProps) => { const { data, isLoading } = trpc.getFileUploadStatus.use ...

NestJS endpoint throwing a 500 error after submitting a post request

Struggling with sending post requests in NestJS as they are returning an error message: ERROR: An error occurred in POST /api/fraud-rules in 8ms... { "isError": true, "status": 500, "name": "InternalError", & ...

Using prevState in setState is not allowed by TypeScript

Currently, I am tackling the complexities of learning TypeScipt and have hit a roadblock where TS is preventing me from progressing further. To give some context, I have defined my interfaces as follows: export interface Test { id: number; date: Date; ...

Guide on linking an XML reply to TypeScript interfaces

Currently, I am faced with the task of mapping an XML response (utilizing text XMLHttpRequestResponseType) from a backend server to a TypeScript interface. My approach has been to utilize xml2js to convert the XML into JSON and then map that JSON to the Ty ...

Retrieve a variable in a child component by passing it down from the parent component and triggering it from the parent

I'm struggling to grasp this concept. In my current scenario, I pass two variables to a component like this: <app-selectcomp [plid]="plid" [codeId]="selectedCode" (notify)="getCompFromChild($event)"></app-select ...

What is the purpose of having a tsconfig.json file in every subdirectory, even if it just extends the main configuration file?

My goal is to streamline the configuration files in my front-end mono repo by utilizing Vite with React and TypeScript. At the root of my repository, I have set up a tsconfig.json file that contains all the necessary settings to run each project, including ...

Is it possible to implement typed metaprogramming in TypeScript?

I am in the process of developing a function that takes multiple keys and values as input and should return an object with those keys and their corresponding values. The value types should match the ones provided when calling the function. Currently, the ...

Transforming the data type of a variable

Recently, I decided to switch my file name from index.js to index.ts. Here's an example of the issue I'm facing: let response = "none" let condition = true if(condition){ response = {id: 123 , data: []} } console.log(response) Howev ...

The module "node_modules/puppeteer/lib/types" does not contain the export "Cookie"

Currently facing an issue with puppeteer types. I am attempting to import the Cookie type, but it seems to be not functioning on versions above 6.0.0. import { Cookie } from 'puppeteer'; Here is the error message: /node_modules/puppeteer/lib/typ ...

Angular form requests can utilize the Spring Security Jwt Token to allow all options methods

Despite checking numerous sources online, I am facing a persistent issue with my Angular application. The problem arises when using HttpClient along with an Angular interceptor to set headers for authentication in my Java Rest API using JWT tokens. The int ...

When the typeof x is determined to be "string", it does not result in narrowing down to just a string, but rather to T & string

Could someone help me understand why type narrowing does not occur in this specific case, and return typing does not work without using: as NameOrId<T>; Is there a more efficient way to rewrite the given example? Here is the example for reference: ...

Returning a value with an `any` type without proper validation.eslint@typescript-eslint/no-unsafe-return

I am currently working on a project using Vue and TypeScript, and I am encountering an issue with returning a function while attempting to validate my form. Below are the errors I am facing: Element implicitly has an 'any' type because expression ...