Excel reports,built to spec.
Type-safe schemas that render polished XLSX reports for TypeScript apps. Buffered or streamed, with zero runtime dependencies.
- schema
- createExcelSchema<Invoice>()
- parts
- 6 layers, 1 schema
- scale
- 1 : 1,048,576 rows
Same schema. Any size.
1createWorkbook()bufferedcreateWorkbookStream()streamingschema: invoiceSchemaunchanged11 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
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.
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.
.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")],})
- ExportYour code bakes the values and writes the formula into the file.
- Open in ExcelOnly the file travels. Your TypeScript stays on your server.
- Edit a quantitySomeone changes Okapi Health from 4 to 6.
- 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.
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: ,})
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.
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 }), ], }));
"region" holds text. Only number columns can be summed, such as "revenue". - TableTwelve orders on their own sheet, typed by their schema.
- Pivot tableRevenue summed by region and half. Summing a text column won't compile.
- Pivot chartA native chart drawn from the pivot. Excel keeps all three in sync.
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.
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.