typed-xlsx
Columns

Summary Rows

Reducer-based summary rows — cell, formula, label, spacer, and empty — for report-mode schemas.

Summaries are post-data rows that appear below a table. They are report-mode only — for excel-table mode, use the totals row feature instead.

Every summary is defined as a reducer: init, step, and finalize. This means the exact same schema works in both buffered and streaming mode — step() is called once per row, with no rows held in memory.

Styling support depends on the summary kind:

  • reducer summaries support static style, dynamic style(value), and conditionalStyle
  • formula summaries support static style and conditionalStyle

For the styling models behind those fields, see Cell Styles and Conditional Styles.

Summary cell

summary.cell() reduces row data into a computed value:

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
invoiceId
: string;
amount
: number;
taxRate
: number;
}>() .
column
("invoiceId", {
header
: "Invoice #",
accessor
: "invoiceId" })
.
column
("amount", {
header
: "Amount",
accessor
: "amount",
style
: {
numFmt
: "$#,##0.00" },
summary
: (
summary
) => [
summary
.
cell
({
init
: () => 0,
step
: (
acc
,
row
) =>
acc
+
row
.
amount
,
finalize
: (
acc
) =>
acc
,
style
: {
numFmt
: "$#,##0.00",
font
: {
bold
: true } },
}), ], }) .
column
("tax", {
header
: "Tax",
accessor
: (
row
) =>
row
.
amount
*
row
.
taxRate
,
style
: {
numFmt
: "$#,##0.00" },
summary
: (
summary
) => [
summary
.
cell
({
init
: () => 0,
step
: (
acc
,
row
) =>
acc
+
row
.
amount
*
row
.
taxRate
,
finalize
: (
acc
) =>
acc
,
style
: {
numFmt
: "$#,##0.00",
font
: {
bold
: true } },
}), ], }) .
build
();

Formula summaries

summary.formula() declares an Excel range formula instead of a JavaScript reduction. Use this when you want the summary cell to recalculate in Excel if the underlying data changes.

The column.cells() context gives you .sum(), .average(), .count(), .min(), .max() — each emits the corresponding range formula (e.g. =SUM(C2:C1001)):

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
amount
: number;
qty
: number }>()
.
column
("qty", {
header
: "Qty",
accessor
: "qty",
summary
: (
summary
) => [
summary
.
formula
(({
column
}) =>
column
.
cells
().
sum
())],
}) .
column
("amount", {
header
: "Amount",
accessor
: "amount",
style
: {
numFmt
: "$#,##0.00" },
summary
: (
summary
) => [
// fx is available for wrapping the range formula in another function
summary
.
formula
(({
column
,
fx
}) =>
fx
.
round
(
column
.
cells
().
sum
(), 2)),
], }) .
build
();

Shorthand strings

For the common case, pass a shorthand string instead of a callback:

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
revenue
: number;
units
: number;
price
: number }>()
.
column
("revenue", {
accessor
: "revenue",
summary
: (
summary
) => [
summary
.
formula
("sum")],
}) .
column
("units", {
accessor
: "units",
summary
: (
summary
) => [
summary
.
formula
("count")],
}) .
column
("price", {
accessor
: "price",
summary
: (
summary
) => [
summary
.
formula
("average")],
}) .
build
();

Available shorthands: "sum", "average", "count", "min", "max".

Summary styling

Reducer summaries can compute style from the finalized summary value:

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
amount
: number }>()
.
column
("amount", {
accessor
: "amount",
summary
: (
summary
) => [
summary
.
cell
({
init
: () => 0,
step
: (
acc
: number,
row
) =>
acc
+
row
.
amount
,
finalize
: (
acc
: number) =>
acc
,
style
: (
value
) => ({
numFmt
: "$#,##0.00",
font
: {
bold
: true,
color
: {
rgb
: (
value
as number) >= 0 ? "166534" : "991B1B" },
}, }), }), ], }) .
build
();

Both reducer and formula summaries can also use conditionalStyle for Excel-native live styling:

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
amount
: number }>()
.
column
("amount", {
accessor
: "amount",
summary
: (
summary
) => [
summary
.
formula
("sum", {
style
: {
numFmt
: "$#,##0.00",
font
: {
bold
: true },
},
conditionalStyle
: (
conditional
) =>
conditional
.
when
(({
cell
}) =>
cell
.
current
().
lt
(0), {
fill
: {
color
: {
rgb
: "FEE2E2" } },
font
: {
color
: {
rgb
: "991B1B" },
bold
: true },
}), }), ], }) .
build
();

Labels and alignment

summary.label() places static text in a summary cell. Use it to label what the row represents.

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
category
: string;
units
: number;
revenue
: number;
}>() .
column
("category", {
accessor
: "category",
summary
: (
summary
) => [
summary
.
label
("TOTAL", {
style
: {
font
: {
bold
: true },
alignment
: {
horizontal
: "right" } },
}), ], }) .
column
("units", {
accessor
: "units",
summary
: (
summary
) => [
summary
.
formula
("sum")],
}) .
column
("revenue", {
accessor
: "revenue",
style
: {
numFmt
: "$#,##0.00" },
summary
: (
summary
) => [
summary
.
formula
(({
column
,
fx
}) =>
fx
.
round
(
column
.
cells
().
sum
(), 2))],
}) .
build
();

empty() vs spacer()

These two helpers are easy to confuse, so it helps to treat them separately:

HelperValueDefault summary stylingTypical use
summary.empty()emptyyeskeep a normal summary row shape with no value in this column
summary.spacer()emptynointentionally leave a visual gap without default summary emphasis

Use spacer() when you want a blank cell that does not inherit the default summary emphasis:

import { 
createExcelSchema
} from "typed-xlsx";
const
schema
=
createExcelSchema
<{
orderId
: string;
sku
: string;
qty
: number;
amount
: number;
}>() .
column
("orderId", {
header
: "Order ID",
accessor
: "orderId",
summary
: (
summary
) => [
summary
.
label
("TOTAL")],
}) .
column
("sku", {
accessor
: "sku",
// Spacer: blank cell without default summary styling
summary
: (
summary
) => [
summary
.
spacer
()],
}) .
column
("qty", {
accessor
: "qty",
summary
: (
summary
) => [
summary
.
formula
("sum")],
}) .
column
("amount", {
accessor
: "amount",
style
: {
numFmt
: "$#,##0.00" },
summary
: (
summary
) => [
summary
.
formula
("sum")],
}) .
build
();

Multiple summary rows

Return multiple definitions from the callback to produce multiple summary rows. Each array index maps to the same summary row index across all columns — index 0 is the first summary row, index 1 is the second, and so on.

A practical example: a P&L report with a pre-tax total row and a post-tax total row:

import { 
createExcelSchema
} from "typed-xlsx";
const
plSchema
=
createExcelSchema
<{
lineItem
: string;
amount
: number;
}>() .
column
("lineItem", {
header
: "Line Item",
accessor
: "lineItem",
summary
: (
summary
) => [
summary
.
label
("NET REVENUE", {
style
: {
font
: {
bold
: true } } }),
summary
.
label
("NET REVENUE + TAX", {
style
: {
font
: {
bold
: true } } }),
], }) .
column
("amount", {
header
: "Amount",
accessor
: "amount",
style
: {
numFmt
: "$#,##0.00" },
summary
: (
summary
) => [
summary
.
cell
({
init
: () => 0,
step
: (
acc
,
row
) =>
acc
+
row
.
amount
,
finalize
: (
total
) =>
total
,
style
: {
numFmt
: "$#,##0.00",
font
: {
bold
: true } },
}),
summary
.
cell
({
init
: () => 0,
step
: (
acc
,
row
) =>
acc
+
row
.
amount
,
finalize
: (
total
) =>
total
* 1.2, // +20% VAT
style
: {
numFmt
: "$#,##0.00",
font
: {
bold
: true } },
}), ], }) .
build
();

Summary availability

Summaries are defined at the column level in the schema, but they only exist if:

  • The schema mode is "report" (not "excel-table")
  • The column is selected into the final table
Copyright © 2026 Cyprien Thao. Released under the MIT License.