TanStack Charts is designed so ordinary chart code names its application types once. Data, channel outputs, scale domains, focus points, and framework props then infer from the definition.
import { scaleLinear, scaleTime } from 'd3-scale'
import { defineChart, lineY } from '@tanstack/charts'
interface Reading {
id: string
recordedAt: Date
temperature: number
}
const readings: readonly Reading[] = []
const definition = defineChart({
marks: [
lineY(readings, {
x: 'recordedAt',
y: 'temperature',
key: 'id',
}),
],
x: { scale: scaleTime() },
y: { scale: scaleLinear() },
})Here the datum is Reading, x values are Date, and y values are number. An incompatible scale or channel field fails at the definition instead of surfacing later in a tooltip callback.
function createTrafficDefinition(rows: readonly Reading[]) {
return defineChart({
marks: [
lineY(rows, {
x: 'recordedAt',
y: 'temperature',
key: 'id',
}),
],
x: { scale: scaleTime() },
y: { scale: scaleLinear() },
})
}Framework components should memoize the complete definition:
const definition = useMemo(() => createTrafficDefinition(readings), [readings])
return <Chart definition={definition} ariaLabel="Temperature history" />Definition identity tells the host when captured application data or options changed. A responsive definition callback still rebuilds when the host size changes:
function createTrafficDefinition(rows: readonly Reading[]) {
return defineChart(({ width }) => ({
marks: [
lineY(rows, {
x: 'recordedAt',
y: 'temperature',
key: 'id',
}),
],
x: { scale: scaleTime() },
y: { scale: scaleLinear() },
margin: width < 480 ? 24 : 40,
}))
}Prefer:
Avoid annotating an intermediate object as broad ChartSpec before passing it to defineChart. That discards the literal mark tuple used for axis and callback inference.
Focus and selection callbacks receive the original datum and inferred coordinate types:
<Chart
definition={definition}
ariaLabel="Temperature readings"
onSelect={(point) => {
if (!point) return
point.datum.recordedAt // Date
point.xValue // Date
point.yValue // number
}}
/>Do not cast a callback parameter or re-find its datum by key. If inference has degraded to unknown, move back to the definition and look for an erased mark tuple, an any annotation, or an untyped custom scale.
The public type utilities are useful at extension boundaries:
import type {
ChartMarkDatum,
ChartSpecDatum,
ChartSpecXValue,
ChartSpecYValue,
} from '@tanstack/charts'
import type {
ChartMarkPointX,
ChartMarkPointY,
} from '@tanstack/charts/mark/scale-values'Use them to describe reusable helpers without repeating a datum or coordinate union manually. The exact utility contracts are listed in Types.
createMark<TDatum, TXValue, TYValue> keeps interaction points and scale values aligned for the common case. Use the advanced scale-value factory only when the materialized axis domain intentionally differs from the point anchor.
See Custom Marks and Renderers. A custom extension that requires as unknown as, a private import, or suppressed type errors indicates a missing public boundary and should be reduced to a failing type test.
Keep positive and negative examples near reusable definitions:
// @ts-expect-error chart options do not accept formal input
mountChart(container, {
definition,
input: { rows: readings },
ariaLabel: 'Readings',
})Use @ts-expect-error only when the test asserts a specific rejected contract. Do not use it to make application examples compile.