Skip to content
View as Markdown

Value aggregations ​

aggregations creates reusable callbacks that select values from a row collection and aggregate them. Each callback accepts { rows: readonly TRow[] }; the module has no dependency on React, DataTable, or column definitions.

tsx
import { aggregations, type Aggregation } from "@tailor-platform/app-shell";

type Line = { quantity: number | null };

const sum = aggregations.sum((row: Line) => row.quantity);
sum({ rows: [{ quantity: 1 }, { quantity: 2 }, { quantity: null }] }); // 3
sum({ rows: [{ quantity: 4 }, { quantity: 5 }] }); // 9: no previous aggregate state

const summary: Aggregation<Line, string> = aggregations.sum(
  (row: Line) => row.quantity,
  (value) => (value === null ? "Unavailable" : `${value} units`),
);
summary({ rows: [{ quantity: 1 }, { quantity: 2 }] }); // "3 units"

Operations ​

FactoryResultEmpty or all-nullish selected values
aggregations.sum(selector)Sum of finite numbers0
aggregations.avg(selector)Average of finite numbersnull
aggregations.min(selector)Minimum finite numbernull
aggregations.max(selector)Maximum finite numbernull
aggregations.count(selector)Count of non-nullish values of any type0

A selector is required. It receives one row at a time and determines the values to aggregate. Numeric selectors must return number | null | undefined; count selectors may return any type. There is no automatic field lookup or connection to a column's id, accessor, or rendered content.

Each factory returns an Aggregation<TRow, TResult> callback. Pass a read-only or mutable row array through { rows }; aggregation does not mutate it or retain state between invocations. Selectors and transformers must be pure and must not mutate rows. Exceptions from either callback propagate to the caller.

All operations skip selected null and undefined values. Numeric operations also validate selected values at runtime and return null for the entire result if any other value is not a finite JavaScript number, including numeric strings, booleans, dates, NaN, and infinities. They do not coerce values, compare dates or strings, or return a partial result. Average divides by the number of non-nullish numeric values, not the row count.

Calculations use ordinary JavaScript numbers. A non-finite calculated result returns null; for average, this includes an intermediate sum overflow even when the mathematical average could be finite. These functions do not provide arbitrary-precision arithmetic.

count(selector) counts selected 0, false, and "" as present values. It counts non-nullish values, not truthy values; use rows.length when every row should count.

Optional transformer ​

Pass a second function to transform the final result:

tsx
const averageText = aggregations.avg(
  (row: Line) => row.quantity,
  (value) => value?.toFixed(2) ?? "–",
);
averageText({ rows: [{ quantity: 1 }, { quantity: null }, { quantity: 2 }] }); // "1.50"

const detailed = aggregations.sum(
  (row: Line) => row.quantity,
  (total) => ({ total, valid: total !== null }),
);
detailed({ rows: [{ quantity: 1 }, { quantity: 2 }] }); // { total: 3, valid: true }

The transformer is called exactly once per aggregation invocation, including for empty or invalid input. Numeric transformers receive number | null; count transformers receive number. Its return type becomes the callback's result type, and may be text, JSX, an object, or another value. The aggregation library does not interpret the transformed result.

A transformer receives only the result, not rows, selected values, columns, or UI context. Filter rows before invoking the callback when the calculation needs a different input set.

For detached callbacks, annotate the selector's row parameter as above so the row and transformed result types can both be inferred. Row types are not restricted to objects; for example, aggregations.sum<number>((value) => value)({ rows: [1, 2] }) returns 3.

Composing with business validation ​

An aggregation is an ordinary callback, so a wrapper can validate the supplied rows before calling it. Keep business rules in the wrapper, numeric calculation in the aggregation, and result formatting in the transformer. The transformer is not an error handler: it receives null for invalid numeric results but has no row context or business-error details; selector exceptions still propagate.

For example, reject mixed currencies before adding monetary amounts:

tsx
type MoneyLine = { amount: number | null; currency: string };

const sumAmount = aggregations.sum(
  (row: MoneyLine) => row.amount,
  (total) => (total === null ? "Invalid amount" : total.toLocaleString("en-US")),
);

const moneyFooter = (ctx: { rows: readonly MoneyLine[] }) => {
  const currencies = new Set(
    ctx.rows.filter((row) => row.amount != null).map((row) => row.currency),
  );
  if (currencies.size > 1) {
    return `Mixed currencies: ${[...currencies].join(", ")}`;
  }
  return sumAmount(ctx);
};

moneyFooter({
  rows: [
    { amount: 1000, currency: "JPY" },
    { amount: 500, currency: "JPY" },
  ],
}); // "1,500"
moneyFooter({
  rows: [
    { amount: 1000, currency: "JPY" },
    { amount: 500, currency: "USD" },
  ],
}); // "Mixed currencies: JPY, USD"

The guard ignores nullish amounts because they do not contribute to the sum. A rejected currency combination returns directly without evaluating the selector or transformer. Neither the wrapper nor the helper converts currencies or provides arbitrary-precision arithmetic.

Pass moneyFooter directly as a column's footer. See the runnable DataTable example to toggle mixed currencies and observe the composed behavior.

DataTable integration ​

Pass an aggregation callback directly to a column footer. It is an ordinary footer renderer that reads the current ctx.rows; DataTable needs no aggregation-specific handling:

tsx
import { aggregations, createColumnHelper } from "@tailor-platform/app-shell";

const { column } = createColumnHelper<{ quantity: number | null }>();
column({
  id: "quantity",
  type: "number",
  footer: aggregations.sum((row) => row.quantity),
});

Inline selectors infer the row type from the surrounding footer definition. The selector is the source of truth even when the column has an accessor; write the same calculation in the selector if the footer should aggregate a computed cell value.

Use a transformer for simple footer text or JSX. DataTable only accepts callback results it can render; object-producing aggregations remain available for non-UI use. For the current-page scope, loading behavior, and custom renderers, see DataTable column footers.