Дженерики и типизация входных данных

Базовая модель данных и её значение в типизации

Визуализация в Nivo строится вокруг строго определённых структур данных, которые описывают точки, серии и дополнительные метаданные графика. В основе лежит идея обобщённого (generic) описания данных, где библиотека не фиксирует конкретный тип значений, а предоставляет механизмы для их строгого определения на уровне приложения.

Типовая проблема при работе с графиками заключается в том, что данные могут иметь разную семантику: числовые временные ряды, категориальные оси, вложенные структуры с дополнительными атрибутами. Без дженериков такие данные быстро приводят к потере типобезопасности и появлению any в коде.

В Nivo применяется обобщённый подход:

  • данные параметризуются через дженерики <RawDatum>
  • серии описываются через <Datum>
  • компоненты визуализации принимают тип данных как параметр

Это позволяет сохранять строгую типизацию на всём пути от источника данных до отрисовки графика.


Обобщённые типы данных в Nivo

Основной концепт строится вокруг двух уровней:

  1. RawDatum — исходная структура данных, поступающая в график
  2. ComputedDatum — преобразованная структура, используемая внутри визуализации

Простейшая модель для линейного графика:

type Point = {
    x: string | number | Date
    y: number
}

Использование дженерика в компоненте:

import { ResponsiveLine } from '@nivo/line'

type MyPoint = {
    x: number
    y: number
}

const data: Array<{
    id: string
    data: MyPoint[]
}> = [
    {
        id: 'series-1',
        data: [
            { x: 1, y: 10 },
            { x: 2, y: 20 }
        ]
    }
]

<ResponsiveLine<MyPoint>
    data={data}
/>

Здесь MyPoint выступает параметром дженерика, фиксируя структуру точек.


Типизация серий данных

В большинстве компонентов Nivo используется модель серии:

type Serie<Datum> = {
    id: string | number
    data: Datum[]
}

Такой подход позволяет параметризовать любые графики независимо от структуры точки.

Пример расширенной модели:

type SalesDatum = {
    month: string
    value: number
    region: string
}

type SalesSerie = {
    id: string
    data: SalesDatum[]
    color?: string
}

Передача в компонент:

<ResponsiveLine<SalesDatum>
    data={salesSeries}
/>

Важно, что дженерик применяется не к серии, а к типу точки, поскольку серия является контейнером.


Инференс типов и явное указание дженериков

TypeScript способен выводить типы автоматически, если структура данных не нарушает ожидаемый формат.

const data = [
    {
        id: 'A',
        data: [
            { x: 1, y: 2 },
            { x: 2, y: 3 }
        ]
    }
]

В этом случае тип может быть выведен как:

{
    id: string
    data: { x: number; y: number }[]
}[]

Однако при добавлении дополнительных полей:

{ x: 1, y: 2, label: 'first point' }

вывод типов часто деградирует до расширенных union-структур, что снижает строгость проверки. Поэтому в Nivo-проектах обычно фиксируется явный тип дженерика.


Расширение типов точек (custom datum)

Практическая особенность Nivo заключается в том, что точка графика может содержать произвольные поля, используемые в тултипах, кастомных слоях и логике форматирования.

type MetricDatum = {
    x: number
    y: number
    timestamp: number
    status: 'ok' | 'warning'
}

Использование в визуализации:

<ResponsiveLine<MetricDatum>
    data={data}
    tooltip={({ point }) => {
        return (
            <div>
                <div>{point.data.x}</div>
                <div>{point.data.y}</div>
                <div>{point.data.status}</div>
            </div>
        )
    }}
/>

Тип point.data автоматически наследует MetricDatum, что исключает необходимость кастов.


Дженерики в контексте осей и масштабов

Типизация не ограничивается точками. Оси также зависят от структуры данных.

type AxisDatum = {
    x: Date
    y: number
}

Для временных шкал часто используется:

type TimeSerie = {
    x: Date
    y: number
}

При этом масштабирование требует согласованности типов:

  • xScale ожидает тип оси X
  • yScale работает с числовыми значениями

Несоответствие типов приводит к логическим ошибкам даже при компиляции без ошибок, поэтому строгая фиксация Datum критична.


Типизация bar, pie и других компонентов

Линейные графики используют точечную модель, тогда как другие типы визуализаций требуют иной структуры.

Bar chart:

type BarDatum = {
    category: string
    valueA: number
    valueB: number
}

Pie chart:

type PieDatum = {
    id: string
    label: string
    value: number
}

Использование:

<ResponsivePie<PieDatum>
    data={pieData}
/>

Здесь дженерик фиксирует тип объекта сектора.


Вложенные структуры и сложные типы

В реальных системах данные часто содержат вложенные поля:

type ComplexDatum = {
    x: number
    y: number
    meta: {
        source: string
        confidence: number
    }
}

Nivo позволяет безопасно использовать такие структуры в кастомных компонентах:

<ResponsiveLine<ComplexDatum>
    data={data}
    layers={[
        'grid',
        'axes',
        'lines',
        'points',
        ({ points }) => {
            return points.map(p => {
                const meta = p.data.meta
                return (
                    <g key={p.id}>
                        <text>
                            {meta.source}
                        </text>
                    </g>
                )
            })
        }
    ]}
/>

Тип meta сохраняется на всём пути обработки данных.


Ограничения дженериков и контракт данных

Дженерики в Nivo не являются полностью свободными: они ограничены внутренними интерфейсами библиотеки.

Базовый контракт можно представить так:

interface BaseDatum {
    x: PropertyKey
    y: number
}

При попытке передать несовместимый тип:

type InvalidDatum = {
    x: { a: number }
    y: string
}

возникает нарушение ожиданий визуализации:

  • ось X не может интерпретировать объект
  • ось Y требует числового значения

Типизация помогает выявить такие ошибки до выполнения.


Совместимость с кастомными форматтерами и tooltip-типами

Типизация активно используется в форматтерах:

type TooltipDatum = {
    x: number
    y: number
    label: string
}

const tooltip = (point: { data: TooltipDatum }) => {
    return `${point.data.label}: ${point.data.y}`
}

Типизация гарантирует, что форматтер всегда получает согласованную структуру.


Расширение типов через module augmentation

В TypeScript возможно расширять типы Nivo через декларации:

declare module '@nivo/line' {
    interface Point {
        extra?: string
    }
}

Это позволяет добавлять поля без изменения сигнатур компонентов.


Типовые ошибки при работе с generics

Распространённые проблемы:

  • потеря типизации при использовании any[]
  • несовпадение структуры data и дженерика <Datum>
  • смешивание разных типов точек в одной серии
  • отсутствие фиксации id, приводящее к неоднозначности сериализации

Типовой анти-паттерн:

const data: any = [...]

Он полностью отключает механизм проверки и делает дженерики бессмысленными.


Согласование типов между слоями визуализации

Типизация в Nivo распространяется на несколько уровней:

  • входные данные (raw dataset)
  • вычисленные точки (computed points)
  • визуальные элементы (SVG/Canvas nodes)
  • события взаимодействия (hover, click)

Пример события:

type ClickDatum = {
    id: string
    x: number
    y: number
}
<ResponsiveLine<ClickDatum>
    onCl ick={(point) => {
        const d = point.data
        console.log(d.x, d.y)
    }}
/>

Согласованность типов между слоями обеспечивает предсказуемость поведения всей визуализации.