typed-xlsx

Excel reports,built to spec.

1 row to 1,048,576 rows

Type-safe schemas that render polished XLSX reports for TypeScript apps. Buffered or streamed, with zero runtime dependencies.

Get started
A
B
C
D
E
F
1
2
3
4
5
6
7
8
9
10
Halvorsen Freight
12
480.00
Okapi Health
4
1,250.00
Brightwater Labs
30
96.50
Tanaka Robotics
8
720.00
Quarry Lane Studio
2
3,400.00
Nordvik Energy
16
310.00
Pell Street Bank
6
1,875.00
Due
Overdue
Due
=ROUND(C3*D3,2)5,760.00
=ROUND(C4*D4,2)5,000.00
=ROUND(C5*D5,2)2,895.00
=ROUND(C6*D6,2)5,760.00
=ROUND(C7*D7,2)6,800.00
=ROUND(C8*D8,2)4,960.00
=ROUND(C9*D9,2)11,250.00
Customer
Billing
Signal
Customer
Status
Qty
Unit price
Total
Trend
Total
78
=SUM(E3:E9)42,425.00
NO.PART
INVOICE REPORT
schema
createExcelSchema<Invoice>()
parts
6 layers, 1 schema
scale
1 : 1,048,576 rows
rendered sheet

Same schema. Any size.

1
rows in one sheetone in-memory pass
createWorkbook()buffered
createWorkbookStream()streaming
schema: invoiceSchemaunchanged
MEASURED ON THE STREAMING ENGINE
200,000 rows3.72s274MiB
500,000 rows9.30s279MiB
vs 4.2.0 on 500,000 rows3.3× faster

11 columns with styles, dates, formats and summaries, inline strings, 5,000-row batches. Node 24, Apple M5. Time and peak memory; 200k is the median of three runs.

Effortlessly beautiful.
Down to the cell.

Badges, sparklines, images, validation, grouped headers, totals. Every one is a schema field, so every report your app ships looks the same way.

STRUCTURE

DATA

VISUALS

E4fxAt risk
A
B
C
D
E
F
G
H
I
J
K
L
M
1
2
3
4
5
6
7
8
9
10
Account
Contract
Revenue
Usage %
Health
Account
Owner
Plan
Status
Prior ARR
ARR
Δ ARR
Jul
Aug
Sep
Trend
Signed
Ama Okafor
Enterprise
Renewed
$162,500
$184,000
+$21,500
71
78
84
Léa Moreau
Growth
At risk
$70,800
$62,400
-$8,400
64
58
51
Jonas Lindqvist
Enterprise
Analytics add-on
Renewed
$214,000
$241,900
+$27,900
88
91
95
Sami Haddad
Starter
Pending
$18,600
$18,600
$0
40
44
47
Ama Okafor
Growth
Renewed
$88,000
$96,250
+$8,250
66
70
69
Léa Moreau
Enterprise
Churned
$132,000
$0
-$132,000
52
31
12
Total
$685,900
$603,150
-$82,750
3 of 6
Starter
Growth
Enterprise
10
RenewalsPipelineBoard+
Type safety

Typed against your data. Can't break at runtime.

Your schema is checked against your row type, so typed-xlsx knows what every column serializes to before anything runs. Break the contract and it fails in your editor, with an error that tells you how to fix it.

Your data · serializes as
type Invoice
customer: { name }no cell
customer.name: stringtext
qty: numbernumber
unitPrice: numbernumber
paid: booleancheckbox
An object has no cell type, so it can't be a column on its own.
The contract · your schema
invoices.tsTypeScript
on
Quick fix
No problemsLn 1
What Excel gets · invoices.xlsx
Sheet1Written

Bake it in TypeScript.
Or wire it into Excel.

A column can be computed by your code while the file is written, or shipped as a typed formula that Excel reruns on every edit. The compiler checks both, and one schema can mix them column by column.

report.tsruns on your server
.column("total", {  header: "Total · baked",  accessor: (row) => row.qty * row.unitPrice,}).column("wired", {  header: "Total · wired",  formula: ({ refs }) =>    refs.column("qty").mul(refs.column("unitPrice")),  conditionalStyle: (c) => c.when(    ({ refs }) => refs.column("wired").gt(5000),    { fill: { color: { rgb: "C9F2D9" } } },  ),  summary: (s) => [s.formula("sum")],})
exporting report.xlsxexport done. This code is not in the file.
E3fx
report.xlsx+
baked at export
  1. ExportYour code bakes the values and writes the formula into the file.
  2. Open in ExcelOnly the file travels. Your TypeScript stays on your server.
  3. Edit a quantitySomeone changes Okapi Health from 4 to 6.
  4. RecalculateThe wired total, its highlight and the sum follow. The baked total stays 5,000.

One schema.
Every report.

Pick columns per audience, generate columns from runtime data, switch to a native Excel table, change the theme. The schema stays one file.

Click the highlighted values0 edits
const deals = createExcelSchema<Deal, Ctx>({  mode: ,})  .column("account", { accessor: "account" })  .column("owner", { accessor: "owner.name" })  .column("stage", { type: "badge", accessor: "stage" })  .dynamic("regions", (b, { ctx }) =>    ctx.regions.forEach((r) =>      b.column(r, { accessor: (d) => d.byRegion[r] })))  .column("total", {    accessor: "total",    summary: (s) => [s.formula("sum")],  })  .build() workbook.sheet("Deals").table("deals", {  title: "Q3 deals by region",  rows, schema: deals,  context: { regions: [, , , ] },  // audience     // no select: every column ships  theme: ,})
F9fx=SUM(F3:F8)
A
B
C
D
E
F
1
2
3
4
5
6
7
8
9
Q3 deals by region
Account
Owner
Stage
EMEA
NA
Total
Halvorsen Freight
Ama Okafor
Won
$48,200
$12,400
$70,400
Okapi Health
Léa Moreau
Proposal
$0
$36,500
$48,600
Tanaka Robotics
Jonas Lindqvist
Won
$6,300
$0
$65,000
Quarry Lane Studio
Sami Haddad
Negotiation
$14,100
$22,900
$48,300
Nordvik Energy
Jonas Lindqvist
Won
$52,600
$0
$52,600
Brightwater Labs
Sami Haddad
Proposal
$0
$8,400
$11,500
TOTAL
$121,200
$80,200
$296,400
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DealsNotes+
Rendered from deals.report.ts6 columns, report, forest
New in 4.3

Rows in. Pivots and charts out.
One typed chain.

Pivot tables and charts are declared from the tables your workbook already has, and Excel gets native objects it keeps in sync. TypeScript checks every field on the way: only numbers can be summed or plotted.

workbook.tstyped-xlsx 4.3
createWorkbook()  .sheet("Orders")  .table("orders", { schema: orders, rows })  .sheet("Pivot")  .pivot("byRegion", ({ refs }) =>    refs.sheet("Orders").table("orders").pivot({      rows: ["region"],      columns: ["half"],      values: { revenue: "sum" },    }),  )  .chart("byRegionChart", ({ refs }) => ({    marks: [      refs.pivot("byRegion").bar({ stack: true }),    ],  }));
TS2322 "region" holds text. Only number columns can be summed, such as "revenue".
pivot
Orders12 rows
Pivotpivot table · pivot chart
empty sheetTS2322 nothing is written
Order
Region
Half
Revenue
#1001
EMEA
H1
412k
#1002
Americas
H1
520k
#1003
APAC
H1
261k
#1004
EMEA
H2
498k
#1005
Americas
H2
612k
#1006
APAC
H2
336k
#1007
EMEA
H1
380k
#1008
Americas
H1
545k
#1009
APAC
H1
288k
#1010
EMEA
H2
456k
#1011
Americas
H2
590k
#1012
APAC
H2
302k
region ▾
H1
H2
Total
EMEA
Americas
APAC
Grand total
Revenue by regionH1H200.6M1.2M1.8M2.4MEMEAAmericasAPAC
412k
520k
261k
498k
612k
336k
380k
545k
288k
456k
590k
302k
792k
954k
1,065k
1,202k
549k
638k
  1. TableTwelve orders on their own sheet, typed by their schema.
  2. Pivot tableRevenue summed by region and half. Summing a text column won't compile.
  3. Pivot chartA native chart drawn from the pivot. Excel keeps all three in sync.
bar · line · area · scatter · pie · doughnutcombos with a secondary axischarts across sheetsstreamed workbooks toopivots refresh when Excel opens the file

Read the charts guide or open the revenue review, built with both.

Real workbooks.
Open any of them.

Reports a finance, sales or operations team would actually send, each generated by typed-xlsx and drawn here from the real file. Open one to read the schema that produced it.

revenue-review.xlsxOpen in playground
Loading workbook…
All examples

Zero dependencies.
Its own XLSX engine.

No SheetJS, no ExcelJS. typed-xlsx writes the OOXML parts and the ZIP container itself, so there is nothing else to audit or patch.

$npm i typed-xlsx
Copyright © 2026 Cyprien Thao. Released under the MIT License.