---
url: https://docs.tailor.tech/app-shell/api/aggregations.md
description: >-
  Reusable selector-based sum, average, minimum, maximum, and non-null count
  over row collections, with optional result-only transformation
---

# 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

| Factory                        | Result                                  | Empty or all-nullish selected values |
| ------------------------------ | --------------------------------------- | ------------------------------------ |
| `aggregations.sum(selector)`   | Sum of finite numbers                   | `0`                                  |
| `aggregations.avg(selector)`   | Average of finite numbers               | `null`                               |
| `aggregations.min(selector)`   | Minimum finite number                   | `null`                               |
| `aggregations.max(selector)`   | Maximum finite number                   | `null`                               |
| `aggregations.count(selector)` | Count of non-nullish values of any type | `0`                                  |

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](../components/data-table.md#composing-with-business-validation) 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](../components/data-table.md#column-footers).
