chartjs / Chart.js

Simple HTML5 Charts using the <canvas> tag
https://www.chartjs.org/
MIT License
64.37k stars 11.89k forks source link

Too complex union type when combining TooltipModel with Vue ref #10012

Open henribru opened 2 years ago

henribru commented 2 years ago

Expected Behavior

You should be able to use the TooltipModel type in combination with Vue refs without type issues.

Current Behavior

You get a "Expression produces a union type that is too complex to represent." error in version 3.5.1 and above. 3.5.0 and below works.

Possible Solution

I'm guessing the change that introduced the issue was https://github.com/chartjs/Chart.js/pull/9490, though. Possibly because it made TooltipModel recursive (TooltipModel -> TooltipItem -> Chart -> TooltipModel)? But to be honest I'm not quite sure if this is something that should be fixed on Chart.js' end, Vue's end or even Typescript's end. If you don't feel like it should be fixed here then feel free to close this (but if you have a suggestion for who to pass the buck to in that case that would be appreciated).

Steps to Reproduce

https://stackblitz.com/edit/vitejs-vite-ynfase?file=src%2Fmain.ts&terminal=dev

You'll have to wait for a while, but eventually Typescript gives up evaluating and the foo.value will be marked with the error I mentioned.

Note that the {} as ... stuff is just to simplify the example so I don't have to type out a full actual TooltipModel, the result is the same either way.

Context

I'm storing a TooltipModel in a Vue ref because I'm using a Vue component to create a custom tooltip. It's probably possible for me to work around this issue but perhaps not with full type safety intact, which would be a bit of a shame.

Environment

beeequeue commented 2 years ago

Can confirm this bug, had to downgrade to 3.5.0 to be able to use the package

eirikove commented 1 year ago

A possible workaround is to use shallowRef for the tooltipModel. Note that this removes deep reactivity, but that's okay if you're not patching, but reassigning the tooltipModel.value every time you're rendering your custom tooltip.

https://vuejs.org/api/reactivity-advanced.html#shallowref

Here's a small modified example of how we're rendering a vue component as a tooltip:

<script setup lang="ts">
const tooltipModel = shallowRef<TooltipModel<"line"> | undefined>(undefined);
// ... in chartOptions
 plugins: {
      tooltip: {
        enabled: false,
        external: function ({ tooltip }: { tooltip: TooltipModel<"line"> }) {
          if (tooltip.opacity) {
          // works with shallowRef
            tooltipModel.value = { ...tooltip };
          // doesn't work with shallowRef
            // tooltipModel.value.someProperty = tooltip.someProperty;
            showToolTip.value = true;
            if (timeoutId) {
              clearInterval(timeoutId);
            }
          } else {
            timeoutId = setTimeout(hideToolTip, 200);
          }
        },
      },
    },
</script>
<template>
  <div class="relative h-full w-full" :style="graphHeight">
    <CustomChartTooltip
      :show-tool-tip="showToolTip" 
      :tooltip-model-ref="tooltipModel"
    />
    <canvas v-once :id="canvasId" />
  </div>
</template>

Possibly because it made TooltipModel recursive (TooltipModel -> TooltipItem -> Chart -> TooltipModel)

This assumption and solution kinda makes sense (I think) when looking at the type definitions for how shallowRefs are unwrapped compared to refs. Involves some recursion stuff I don't want to spend too much time trying to understand 😄

@BeeeQueue