Расширение типов при кастомизации

Типизация в Nivo опирается на обобщённые props компонентов и набор базовых интерфейсов, которые описывают данные, оси, серии и визуальные элементы графиков. При кастомизации стандартных компонентов быстро возникает необходимость расширять эти типы, чтобы корректно описывать собственные структуры данных, дополнительные поля и пользовательские рендереры без потери безопасности TypeScript.

Каждый график в Nivo строится вокруг трёх ключевых уровней типов:

  • Datum — структура одного элемента данных
  • Series / Indexed data — группировка элементов
  • Props компонента — конфигурация визуализации

Типы в библиотеке изначально универсальны, например:

type BarDatum = Record<string, number | string>;

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

Причины расширения типов

Расширение типов требуется в следующих случаях:

  • добавление доменных полей в datum (например, id, status, meta)
  • использование сложных вложенных структур
  • кастомные tooltip и label компоненты с доступом к расширенным данным
  • интеграция с API, где данные уже строго типизированы
  • создание переиспользуемых графиков в дизайн-системе

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

Механизм расширения через generics

Большинство компонентов Nivo поддерживает дженерики, позволяя переопределить базовый тип данных:

type MyDatum = {
    id: string;
    value: number;
    status: 'active' | 'inactive';
};

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

import { ResponsiveBar } from '@nivo/bar';

<ResponsiveBar<MyDatum>
    data={data}
    keys={['value']}
    indexBy="id"
/>

Теперь все callback-функции получают строго типизированный datum:

tooltip: ({ data }) => {
    // data: MyDatum
    return `${data.id}: ${data.value} (${data.status})`;
}

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

Когда необходимо изменить поведение библиотечных типов глобально, используется расширение модулей TypeScript.

Пример: расширение базового типа theme.

import '@nivo/core';

declare module '@nivo/core' {
    interface Theme {
        customBranding?: {
            logoUrl: string;
            companyName: string;
        };
    }
}

После этого theme можно использовать с новыми полями:

const theme = {
    customBranding: {
        logoUrl: '/logo.svg',
        companyName: 'DataViz'
    }
};

Расширение типов tooltip и custom layers

Nivo активно использует render props, что требует точного описания входных типов.

type MyTooltipProps = {
    datum: MyDatum;
    color: string;
};

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

const CustomTooltip = ({ datum }: MyTooltipProps) => {
    return (
        <div>
            <strong>{datum.id}</strong>
            <div>{datum.value}</div>
            <div>{datum.status}</div>
        </div>
    );
};

Подключение:

<ResponsiveBar<MyDatum>
    data={data}
    keys={['value']}
    indexBy="id"
    tooltip={CustomTooltip}
/>

Типизация custom layers

Layer API в Nivo позволяет добавлять собственные визуальные слои поверх графика. Типизация здесь критична, поскольку слой получает доступ к internal API графика.

type MyLayerProps = {
    bars: Array<{
        id: string;
        x: number;
        y: number;
        width: number;
        height: number;
        dat a: MyDatum;
    }>;
};

Реализация слоя:

const CustomLayer = ({ bars }: MyLayerProps) => {
    return (
        <>
            {bars.map(bar => (
                <circle
                    key={bar.id}
                    cx={bar.x + bar.width / 2}
                    cy={bar.y}
                    r={4}
                />
            ))}
        </>
    );
};

Подключение:

<ResponsiveBar<MyDatum>
    data={data}
    keys={['value']}
    indexBy="id"
    layers={['grid', 'axes', CustomLayer, 'bars', 'markers']}
/>

Расширение типов осей и форматтеров

Оси в Nivo часто требуют кастомных форматтеров, которые зависят от структуры данных.

type AxisDatum = {
    timestamp: number;
    label: string;
    value: number;
};

Типизация форматтера:

const formatAxis = (value: number): string => {
    return new Date(value).toLocaleDateString();
};

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

axisBottom={{
    format: formatAxis
}}

Обобщение типов через wrapper-компоненты

В крупных проектах часто создаются обёртки над Nivo-компонентами с фиксированными типами.

type AppBarProps = {
    data: MyDatum[];
};

const AppBar = (props: AppBarProps) => {
    return (
        <ResponsiveBar<MyDatum>
            {...props}
            keys={['value']}
            indexBy="id"
        />
    );
};

Такой подход фиксирует контракт данных на уровне дизайн-системы.

Использование mapped types для расширения данных

При сложных графиках часто требуется автоматически расширять типы:

type WithMeta<T> = T & {
    meta?: {
        source: string;
        updatedAt: string;
    };
};

Применение:

type ExtendedDatum = WithMeta<MyDatum>;

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

Типизация событий взаимодействия

Nivo предоставляет события hover/click, которые также можно расширять.

type BarClickEvent = {
    datum: MyDatum;
    id: string;
    value: number;
};
onCl ick={(bar) => {
    const event: BarClickEvent = {
        datum: bar.data as MyDatum,
        id: bar.id,
        value: bar.value
    };
}}

Проблемы несовпадения inferred типов

При использовании сложных данных TypeScript иногда выводит слишком широкие типы:

  • string | number вместо конкретных union
  • потеря literal-типа ключей
  • расширение до Record<string, any>

Решение заключается в явном описании generics на уровне компонента и вынесении типов данных в отдельные модули.

Расширение типов для pie chart

Pie chart использует собственную модель:

type MyPieDatum = {
    id: string;
    label: string;
    value: number;
    category: 'A' | 'B' | 'C';
};
<ResponsivePie<MyPieDatum>
    data={data}
    value="value"
    id="id"
/>

Tooltip получает строгую типизацию:

tooltip={({ datum }) => {
    // datum: ComputedDatum<MyPieDatum>
    return `${datum.label}: ${datum.value}`;
}}

Интеграция расширенных типов в дизайн-систему

При построении дизайн-системы типы Nivo часто становятся частью общей модели данных:

export interface ChartDatumBase {
    id: string;
    label: string;
}

export interface ChartDatum extends ChartDatumBase {
    value: number;
    color?: string;
}

Далее все графики используют единый контракт:

<ResponsiveBar<ChartDatum> />
<ResponsiveLine<ChartDatum> />
<ResponsivePie<ChartDatum> />

Это позволяет унифицировать обработку данных на уровне API и UI без дублирования типов.

Декомпозиция типов для переиспользования

При масштабировании приложения типы разбиваются на слои:

  • domain types — бизнес-логика
  • chart adapters — преобразование данных
  • nivo bindings — привязка к библиотеке

Пример адаптера:

type ApiUser = {
    user_id: string;
    activity_score: number;
};

type ChartUser = {
    id: string;
    value: number;
};

const adapt = (u: ApiUser): ChartUser => ({
    id: u.user_id,
    value: u.activity_score
});

Расширение типов через conditional types

Для сложных случаев используются conditional types:

type ExtractValue<T> = T extends { value: infer V } ? V : never;

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

Типизация render props и фабрик компонентов

Nivo активно использует фабрики:

type BarFactory<T> = (datum: T) => React.ReactNode;
const renderBar: BarFactory<MyDatum> = (datum) => {
    return <div>{datum.value}</div>;
};

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