Home > database >  Why is this type union resolved as an intersection, in typescript?
Why is this type union resolved as an intersection, in typescript?

Time:03-02

I'm having trouble understanding the mechanism behind a typescript error I'm getting - I'm convinced there's something basic doing a "whoosh" sound as it flies over my head.

I've only found this question on the topic, but while the error looks the same, the setup seems quite different to me.

This is the definition I'm referencing, cut for brevity.

type ApexAxisChartSeries = {
  // ...
  data:
    | (number | null)[]
    | {
        x: any;
        y: any;
        // ...
      }[]
    | [number, number | null][]
    | [number, (number | null)[]][];
}[]

And this is the bad function, which takes an {x:number, y:number} array.

function composeSeries(input: { x: number; y: number }[]) {
  const series: ApexAxisChartSeries = [
    { name: "A", data: [] },
    { name: "B", data: [] },
  ];

  input.forEach((datum) => {
    series[0].data.push(datum); // <-- This line right here gives the error.
  });

  return series;
}

Now, what's confusing me is that given the type definition, I would expect a {x:number, y:number} object to be a perfectly valid element of data, but typescript is complaining that

Argument of type '{ x: number; y: number; }' is not assignable to parameter of type number & { x: any; y: any; fillColor?: string | undefined; strokeColor?: string | undefined; meta?: any; goals?: any; } & [number, number | null] & [number, (number | null)[]]'.

Type '{ x: number; y: number; }' is not assignable to type 'number'.

And I'm here like, where in the bleep bloop are those intersections in the error coming from, if the type definition uses unions?

As additional context:

  1. It's not only the object that gives an error. If I use [x,y] tuples or just a number array, the compiler still perks up
  2. I'm using TS 4.5.4
  3. I'm quite positive the issue exists in this code: the TS playground gives the same error

For reference, this is the full type definition:

type ApexAxisChartSeries = {
  name?: string
  type?: string
  color?: string
  data:
    | (number | null)[]
    | {
        x: any;
        y: any;
        fillColor?: string;
        strokeColor?: string;
        meta?: any;
        goals?: any;
      }[]
    | [number, number | null][]
    | [number, (number | null)[]][];
}[]

Any clarification is deeply appreciated!

Edit: an example of the error can be found on the TS playground here, courtesy of @jcalz

CodePudding user response:

There is a difference between (T | U)[] and T[] | U[]. In the first, each array element can be of type T or U. In the second (which corresponds with your case) the array can only have elements of one of either type.

The question you have to ask yourself is this: How can TypeScript know what elements are allowed when it doesn't know which type of an array a variable is?

const myArray: string[] | number[] = [];
myArray.push(5); // Argument of type 'number' is not assignable to parameter of type 'never'.

Take the example above. TypeScript will throw an error no matter because whatever you push to that array will not comply with one of the possible types.

The good thing is that the TypeScript compiler is clever has heck and figured out in your case that a type can be derived that will comply with every possible condition. This type happens to be the intersection of all possible element types, which is the most basic type that would meet the requirements.

Your options are either make the array allow any of the possible types, or make the series a generic that extends any one of the types.

Option A: Array element can be one of many types

type ApexAxisChartSeries = {
  name: string,
  data:
    (number 
      | null 
      | { x: any; y: any; } 
      | [number, number | null]
      | [number, (number | null)[]]
    )[]
};

Option B: Generic type parameter can be one of many types

type ChartDataType = (number | null)
  | { x: any; y: any; } 
  | [number, number | null] 
  | [number, (number | null)[]];

type ApexAxisChartSeries<T extends ChartDataType> = {
  name: string,
  data: T[]
}[];

function composeSeries<T extends ChartDataType>(input: T[]) {
  const series: ApexAxisChartSeries<T> = [
    { name: "A", data: [] },
    { name: "B", data: [] },
  ];

  input.forEach((datum) => {
    const index = (true) ? 0 : 1;
    series[index].data.push(datum);
  });

  return series;
}

composeSeries([{ x:1, y: 1}]); // returns { name, data: { x, y }[] }[]
  • Related