Типизация в Nivo опирается на обобщённые props компонентов и набор базовых интерфейсов, которые описывают данные, оси, серии и визуальные элементы графиков. При кастомизации стандартных компонентов быстро возникает необходимость расширять эти типы, чтобы корректно описывать собственные структуры данных, дополнительные поля и пользовательские рендереры без потери безопасности TypeScript.
Каждый график в Nivo строится вокруг трёх ключевых уровней типов:
Типы в библиотеке изначально универсальны, например:
type BarDatum = Record<string, number | string>;
Такой подход позволяет быстро начать работу, но при усложнении модели данных приводит к потере строгости.
Расширение типов требуется в следующих случаях:
id,
status, meta)Без расширения типов любые дополнительные поля становятся
any, что ломает автодополнение и проверку корректности.
Большинство компонентов 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})`;
}
Когда необходимо изменить поведение библиотечных типов глобально, используется расширение модулей 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'
}
};
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}
/>
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
}}
В крупных проектах часто создаются обёртки над Nivo-компонентами с фиксированными типами.
type AppBarProps = {
data: MyDatum[];
};
const AppBar = (props: AppBarProps) => {
return (
<ResponsiveBar<MyDatum>
{...props}
keys={['value']}
indexBy="id"
/>
);
};
Такой подход фиксирует контракт данных на уровне дизайн-системы.
При сложных графиках часто требуется автоматически расширять типы:
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
};
}}
При использовании сложных данных TypeScript иногда выводит слишком широкие типы:
string | number вместо конкретных unionRecord<string, any>Решение заключается в явном описании generics на уровне компонента и вынесении типов данных в отдельные модули.
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:
type ExtractValue<T> = T extends { value: infer V } ? V : never;
Применение в графиках позволяет автоматически извлекать числовые значения из различных структур данных без потери типобезопасности.
Nivo активно использует фабрики:
type BarFactory<T> = (datum: T) => React.ReactNode;
const renderBar: BarFactory<MyDatum> = (datum) => {
return <div>{datum.value}</div>;
};
Такой подход делает кастомизацию полностью типобезопасной и переносимой между разными графиками.