typed-xlsx
Getting Started

Quick Start

Build an invoice report with formula columns, a totals row and status badges, then export it buffered and streamed from the same schema.

This guide builds one invoice report and exports it twice: once from an array, once streamed in batches. The schema doesn't change between the two.

1. Describe the rows

type 
Invoice
= {
invoiceId
: string;
customer
: string;
qty
: number;
unitPrice
: number;
taxRate
: number;
status
: "paid" | "pending" | "overdue";
};

2. Build the schema

Each .column() adds a column. accessor reads a field, formula writes an Excel formula, summary adds a row below the data, and type: "badge" renders a status as a colored pill.

import { 
createExcelSchema
} from "typed-xlsx";
const
money
= {
numFmt
: "$#,##0.00" };
export const
invoiceSchema
=
createExcelSchema
<
Invoice
>()
.
column
("invoiceId", {
header
: "Invoice #",
accessor
: "invoiceId" })
.
column
("customer", {
header
: "Customer",
accessor
: "customer" })
.
column
("qty", {
header
: "Qty",
accessor
: "qty" })
.
column
("unitPrice", {
header
: "Unit price",
accessor
: "unitPrice",
style
:
money
})
.
column
("subtotal", {
header
: "Subtotal",
formula
: ({
refs
,
fx
}) =>
fx
.
round
(
refs
.
column
("qty").
mul
(
refs
.
column
("unitPrice")), 2),
style
:
money
,
summary
: (
s
) => [
s
.
formula
("sum")],
}) .
column
("taxRate", {
header
: "Tax",
accessor
: "taxRate",
style
: {
numFmt
: "0%" } })
.
column
("total", {
header
: "Total",
formula
: ({
refs
,
fx
}) =>
fx
.
round
(
refs
.
column
("subtotal").
mul
(
fx
.
literal
(1).
add
(
refs
.
column
("taxRate"))),
2, ),
style
:
money
,
summary
: (
s
) => [
s
.
formula
("sum")],
}) .
column
("status", {
header
: "Status",
type
: "badge",
accessor
: "status",
variants
: {
paid
: {
style
: {
fill
: {
color
: {
rgb
: "DCFCE7" } },
font
: {
color
: {
rgb
: "166534" } },
}, },
pending
: {
style
: {
fill
: {
color
: {
rgb
: "FEF3C7" } },
font
: {
color
: {
rgb
: "92400E" } },
}, },
overdue
: {
style
: {
fill
: {
color
: {
rgb
: "FEE2E2" } },
font
: {
color
: {
rgb
: "991B1B" } },
}, }, }, }) .
build
();

Try a typo: refs.column("subtota") or accessor: "unitprice" is a type error, because both are checked against the schema and the Invoice type.

3. Export it from an array

import { 
createWorkbook
} from "typed-xlsx";
const
rows
:
Invoice
[] = [
{
invoiceId
: "INV-001",
customer
: "Acme Corp",
qty
: 10,
unitPrice
: 299,
taxRate
: 0.2,
status
: "paid",
}, {
invoiceId
: "INV-002",
customer
: "Globex Inc",
qty
: 5,
unitPrice
: 850,
taxRate
: 0.2,
status
: "pending",
}, {
invoiceId
: "INV-003",
customer
: "Initech",
qty
: 2,
unitPrice
: 1200,
taxRate
: 0.2,
status
: "overdue",
}, ]; const
workbook
=
createWorkbook
();
workbook
.
sheet
("Invoices", {
freezePane
: {
rows
: 2 } }).
table
("invoices", {
title
: "Invoices, Q1 2025",
rows
,
schema
:
invoiceSchema
,
}); await
workbook
.
writeToFile
("./invoices.xlsx");
// or: workbook.toBuffer() for an HTTP response or an S3 upload

This is the sheet it writes:

A
B
C
D
E
F
G
H
1
2
3
4
5
6
Invoices, Q1 2025
Invoice #
Customer
Qty
Unit price
Subtotal
Tax
Total
Status
INV-001
Acme Corp
10
299
=ROUND((C3*D3),2)
20%
=ROUND((E3*(1+F3)),2)
paid
INV-002
Globex Inc
5
850
=ROUND((C4*D4),2)
20%
=ROUND((E4*(1+F4)),2)
pending
INV-003
Initech
2
1,200
=ROUND((C5*D5),2)
20%
=ROUND((E5*(1+F5)),2)
overdue
=SUM(E3:E5)
=SUM(G3:G5)
1
2
Invoices
  1. 1E3:E5subtotal is a live formula, recalculated when someone edits Qty or Unit price
  2. 2E6:G6summary rows add SUM formulas under the data

Rows 1 and 2 (title and headers) are frozen, so they stay visible while scrolling.

4. Stream it in batches

When the rows come from a database cursor or a paginated API, use createWorkbookStream() with the same schema and commit pages as they arrive:

import { 
createWorkbookStream
} from "typed-xlsx";
const
workbook
=
createWorkbookStream
();
const
table
= await workbook
.
sheet
("Invoices", {
freezePane
: {
rows
: 2 } })
.
table
("invoices", {
title
: "Invoices, Q1 2025",
schema
:
invoiceSchema
});
for await (const
page
of
fetchInvoicePages
()) {
await
table
.
commit
({
rows
:
page
});
} await
workbook
.
writeToFile
("./invoices.xlsx");

The file is the same; the summary row is computed across every committed batch. Streaming supports every schema feature, including formulas, summary rows, sub-row expansion and excel-table mode.

Next steps

Copyright © 2026 Cyprien Thao. Released under the MIT License.