Tips on inferring a distinct generic type for every element within an array of objects

I'm working on code that creates a timeline chart for data. I want the code to accept an array of series, each containing data and various getters used to position and render the data on the chart.

Below is a simplified example of the code. The actual implementation would involve multiple getters on DataSeries<T> and rendering with React, but those details are not relevant to my current issue.

export interface DataSeries<T> {
  data: T[];
  asNumber: (d: T) => number;
}

export function DataSeriesAsNumbers<T>(series: DataSeries<T>[]) {
  series.forEach((s) => {
    s.data.forEach((d) => {
      console.log(s.asNumber(d).toFixed(0));
    });
  });
}

My problem arises when using the function and trying to infer the parameter type for the getNumber callbacks based on the data provided in the accompanying data property.

DataSeriesAsNumbers([
  {
    data: [
      { val: 1, seq: 12346 },
      { val: 2, seq: 12347 },
    ],
    asNumber: (d) => d.val,
  },
  {
    data: [
      { amount: 6, type: 5 },
      { amount: 20, type: 4 },
    ],
    asNumber: (d) => d.amount,
  },
]);

So far, I haven't been able to achieve this automatically. I've tried manually specifying types or disregarding types altogether by using

DataSeriesAsNumbers<T = any>
.

The issue seems to stem from the series: DataSeries<T>[] part, where mixing different objects in the data properties requires all getters to adhere to the same union type. This results in assigning a union type to the d parameter:

https://i.sstatic.net/zJfbX.png

I've explored using the infer keyword without success, and have come across syntax like [...T] online but haven't been able to implement it successfully.

Is there a way to automatically infer the parameter type for callbacks based on the associated data array?

Answer №1

If you need to create a mapped type for an array/tuple type, you can achieve it like this:

function RenderDataChart<T extends any[]>(
  series: [...{ [I in keyof T]: DataSeries<T[I]> }]
) {
  series.forEach(<TI,>(s: DataSeries<TI>) => {
    s.data.forEach((d) => {
      const x = s.getX(d);
      const y = s.getY(d);
      const color = s.getColor(d);
      render(x, y, color, d) // implementing rendering logic
    });
  })
}

The mapped type

{[I in keyof T]: DataSeries<T[I]>}
wraps each element T[I] from the tuple type T with DataSeries<>. For instance, if T is [X, Y, Z], then the mapped type will be
[DataSeries<X>, DataSeries<Y>, DataSeries<Z>]
. The compiler automatically infers T based on this mapped type.

Additionally, enclosing the mapped type within a variadic tuple type like [...+] ensures that the compiler interprets the series input as a tuple type rather than a regular array. This distinction helps maintain behavior consistency.


While TypeScript currently lacks contextual type inference for callback parameters when inferred from a mapped type, there is a feature request open for this improvement at microsoft/TypeScript#53018:

RenderDataChart([
  {
    data: [{ val: 1, seq: 12346 }, { val: 2, seq: 12347 }],
    getX: (d) => d.val, // error
    getY: (d) => d.seq, // error
    getColor: (d) => d.val === 0 ? "red" : "blue", // error
  },
  {
    data: [{ amount: 6, type: 5 }, { amount: 20, type: 4 }],
    getX: (d) => d.amount, // error
    getY: (d) => d.type, // error
    getColor: () => "green", 
  },
]);
// Inferred type of T: [{ val: number; seq: number;}, { amount: number; type: number;}]>

Until an enhancement is made to address this issue, using a generic helper function for callback parameter inference can provide a workaround. For example:

const h = <T,>(x: DataSeries<T>) => x;

This function facilitates correct inference, as demonstrated below:

RenderDataChart([
  h({
    data: [{ val: 1, seq: 12346 }, { val: 2, seq: 12347 }],
    getX: (d) => d.val,
    getY: (d) => d.seq,
    getColor: (d) => d.val === 0 ? "red" : "blue",
  }),
  h({
    data: [{ amount: 6, type: 5 }, { amount: 20, type: 4 }],
    getX: (d) => d.amount,
    getY: (d) => d.type,
    getColor: () => "green",
  }),
]);
// Inferred type of T: [{ val: number; seq: number;}, { amount: number; type: number;}]>

With this approach, you achieve precise typing and proper contextual inference for the callback parameters d.

Access the code on the TypeScript Playground

Answer №2

Special thanks to jcalz's insightful comment, I have managed to create a somewhat functioning version of the solution. If they are willing to provide an answer, I eagerly await and will accept it, hoping for an even better approach than my current one.

By utilizing a helper function to circumvent the issue described in ms/TS#53018, we can establish type inference per dataseries object. Otherwise, the parameters default to any. Initially prompted by my linter warning, I pondered if there was a way to enforce the use of the helper function through a branded type, which I have included in the code snippet below.

Here is the initial version without a branded type:

export interface DataSeries<T> {
  data: T[];
  asNumber: (d: T) => number;
}

export function RenderDataChart<T extends DataSeries<any>>(series: T[]) {
  series.forEach((s) => {
    s.data.forEach((d) => {
      console.log(s.asNumber(d).toFixed(0));
    });
  });
}

export function createDataSeries<DataType>(
  series: DataSeries<DataType>
): DataSeries<DataType> {
  return series;
}

RenderDataChart([
  {
    data: [
      { val: 1, seq: 12346 },
      { val: 2, seq: 12347 },
    ],
    asNumber: (d) => d.val, // d: any
  },
  {
    data: [
      { amount: 6, type: 5 },
      { amount: 20, type: 4 },
    ],
    asNumber: (d) => d.amount, // d: any
  },
  createDataSeries({
    data: [
      { val: 1, seq: 12346 },
      { val: 2, seq: 12347 },
    ],
    asNumber: (d) => d.val, // d: { val: number; seq: number; }
  }),
  createDataSeries({
    data: [
      { amount: 6, type: 5 },
      { amount: 20, type: 4 },
    ],
    asNumber: (d) => d.amount, // d: { amount: number; type: number; }
  }),
]);

Now, introducing the usage of a branded type (excerpt from example code):

export interface IDataSeries<T> {
  data: T[];
  asNumber: (d: T) => number;
}

export type DataSeries<T> = IDataSeries<T> & { 
  // @see createDataSeries
  __dataseries: never
};

export function RenderDataChart<T extends DataSeries<any>>(series: T[]) {
  series.forEach((s) => {
    s.data.forEach((d) => {
      console.log(s.asNumber(d).toFixed(0));
    });
  });
}

export function createDataSeries<DataType>(
  series: IDataSeries<DataType>
): DataSeries<DataType> {
  return series as DataSeries<DataType>;
}

RenderDataChart([
  // Errors:
  {
    data: [
      { val: 1, seq: 12346 },
      { val: 2, seq: 12347 },
    ],
    asNumber: (d) => d.val,
  },
  // Works:
  createDataSeries({
    data: [
      { val: 1, seq: 12346 },
      { val: 2, seq: 12347 },
    ],
    asNumber: (d) => d.val, // d: { val: number; seq: number; }
  }),
]);

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

Experiencing a type error within Redux in a React Native project utilizing TypeScript

I am currently working on implementing a feature to store the boolean value of whether a phone number is verified or not. Within my login component: await dispatch(setOTPVerified(data.is_phone_verified)); Action.tsx: export const OTP_VERIFIED = 'OTP ...

Error Found: Unexpected Colon (:) in Vue TypeScript File

Important Update: After thorough investigation, it appears that the issue is directly related to the boilerplate being used. As a temporary solution, it is recommended not to extract the TypeScript file but keep it within the .vue file for now. In a sim ...

Prevent coverage tracking for files or paths enclosed in square brackets in jest

I am trying to exclude a specific file from test coverage in Jest by modifying the collectCoverageFrom array. The file name contains square brackets, and I have added an entry with a negation for this file. collectCoverageFrom: [ './src/**/*.{js ...

Incorporating optional fields into the form builder without being mandatory

For my current project on Ionic 4, I have implemented a form builder to create and validate forms. I have also included the [disabled] attribute in the form to disable it if all fields are not valid. However, I noticed that even if I do not add Validators ...

Using TypeScript to Initialize Arrays with Objects

Why is it that in TypeScript 1.8, the following code blocks with initializers are considered legal syntax: class A { public textField: string; } var instanceOfClass = new A { textField = "HELLO WORLD" }; var arrayCollection = new A[] { new A ...

Utilize a variable from one Angular component in another by sharing it between .ts files

My issue involves dynamically adding items to a todo list and wanting to exclude certain items. The challenge lies in the fact that the list itself is located outside of the task component: Within the task.component.html file, I iterate through the list f ...

Guide on linking enum values with types in TypeScript

My enum type is structured as follows: export enum API_TYPE { INDEX = "index_api", CREATE = "create_api", SHOW = "show_api", UPDATE = "update_api", DELETE = "destroy_api" }; Presently, I have a f ...

The NextAuth getServerSession function is functional when used with a POST API method, but it

Encountering an issue where getServerSession functions correctly on a POST route but not on a GET route. import { getServerSession } from "next-auth" import { authOptions } from "../../auth/[...nextauth]/route" import { NextResponse } f ...

Stop webpack from stripping out the crypto module in the nodejs API

Working on a node-js API within an nx workspace, I encountered a challenge with using the core crypto node-js module. It seems like webpack might be stripping it out. The code snippet causing the issue is: crypto.getRandomValues(new Uint32Array(1))[0].toS ...

Angular2 tutorial with VS2015 may encounter a call-signature error that is expected

Currently following the Angular2 tutorial in VS2015 and encountering an issue with a warning that is impeding the compilation of one of my TypeScript files. The link to the tutorial is provided below. https://angular.io/docs/ts/latest/tutorial/toh-pt4.htm ...

Utilizing PropTypes in React with TypeScript

I've encountered issues while trying to integrate PropTypes with Typescript: Previously, without typescript, I had successfully used: class TodoFilterItem extends Component { constructor (props) { super(props); Followed by: TodoFilterItem.prop ...

Tips for choosing and unchoosing rows in angular 6

I am looking to extract the values from selected rows and store them in an array. However, I also need to remove a row from the result array when it is deselected. The issue with my current code is that every time I click on a row, the fileName values are ...

The mat-table's data source is failing to refresh and display the latest

When I click on a column header to sort the table, a function should trigger and update the data. However, the data is not updating as expected. Below is the code for the function and the table: <table mat-table #table [dataSource]="dataSourceMD&qu ...

Click to alter the style of an <li> element

I'm currently using Angular CLI and I have a menu list that I want to customize. Specifically, I want to change the background color of the <li> element when it is clicked. I am passing an id to the changeColor() function, but unfortunately, I a ...

Does the JavaScript Amazon Cognito Identity SDK offer support for the Authorization Code Grant flow?

Is there a way to configure and utilize the Amazon Cognito Identity SDK for JavaScript in order to implement the Authorization Code Grant flow instead of the Implicit Grant flow? It appears that the SDK only supports Implicit Grant, which means that a Clie ...

What is the reason why modifying a nested array within an object does not cause the child component to re-render?

Within my React app, there is a page that displays a list of item cards, each being a separate component. On each item card, there is a table generated from the nested array objects of the item. However, when I add an element to the nested array within an ...

Choose a specific interface in Typescript

I am interested in developing a React alert component that can be customized with different message colors based on a specific prop. For example, I envision the component as <alert id="component" info/> or <alert id="component" ...

Encountering a problem while bringing in screens: 'The file screens/xxx cannot be located within the project or any of these folders.'

I am currently working on an iOS application using React Native technology. During the process of importing a specific screen, I encountered an error message. Can anyone provide guidance on how to resolve this issue? Error: Unable to resolve module scree ...

Can the Date class be expanded by overloading the constructor method?

In my dataset, there are dates in different formats that Typescript doesn't recognize. To address this issue, I developed a "safeDateParse" function to handle extended conversions and modified the Date.parse() method accordingly. /** Custom overload ...

Issues with using hooks in a remote module in Webpack 5 module federation

I am attempting to create a dynamic system at runtime using Module Federation, a feature in webpack 5. Everything seems to be working well, but I encounter a multitude of 'invalid rule of hooks' errors when I add hooks to the 'producer' ...