Type-safe, schema-driven Excel reporting for TypeScript.
Define one schema, then generate polished buffered exports or stream large workbooks with the same API.
If the export definition is wrong, the compiler tells you before the spreadsheet does.
Previously published as @chronicstone/typed-xlsx. The canonical package name is typed-xlsx.
npm install typed-xlsx
// before import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx"; // after import { createExcelSchema, createWorkbook } from "typed-xlsx";
- One schema API for buffered exports and streaming exports
- Report mode for custom layouts, summaries, and sub-row expansion
- Excel table mode for native tables, totals rows, autoFilter, and structured refs
- Renderer columns for badges, checkboxes, hyperlinks, sparklines, and embedded or URL-backed images
- Full comparison against SheetJS and ExcelJS: Why typed-xlsx?
Why typed-xlsx
Most XLSX libraries give you a cell API. typed-xlsx gives you a schema API.
- Define columns against your row type with typed path accessors and callback accessors
- Reference columns by ID in formulas instead of hard-coding fragile cell addresses
- Reuse one schema across buffered exports and streaming exports
- Choose between report mode and native Excel table mode without changing the authoring model
- Generate polished workbooks with summaries, grouped headers, sub-row expansion, validation, conditional styles, badges, checkboxes, hyperlinks, sparklines, and product thumbnails
If you are currently evaluating typed-xlsx against lower-level spreadsheet libraries, read the full comparison with SheetJS and ExcelJS:
Why not ExcelJS / SheetJS?
Choose typed-xlsx when your main job is generating typed reports from application data, not manually editing spreadsheets cell by cell.
typed-xlsx: best when you want a schema-first API for report generation, typed formulas, native Excel tables, and streaming with the same authoring modelExcelJS: best when you need lower-level workbook editing and ad hoc worksheet manipulationSheetJS: best when you need parsing, format conversion, or broad spreadsheet interoperability
Read the full tradeoffs and feature matrix here:
Use typed-xlsx when
typed-xlsx is a strong fit when you need to generate Excel files from structured TypeScript data such as:
- financial and operations reports
- SaaS admin exports
- customer success and renewal planning workbooks
- quote review and approval workbooks
- large scheduled exports that need bounded memory usage
Do not use typed-xlsx when
Reach for a lower-level spreadsheet library when you need to:
- read or modify existing
.xlsxfiles - embed charts or arbitrary freeform worksheet drawings
- support spreadsheet formats beyond
.xlsx - do highly manual cell-by-cell spreadsheet editing
The Core API
Three functions cover the main surface:
createExcelSchema()describes columns, formulas, summaries, styles, groups, validation, badges, checkboxes, hyperlinks, sparklines, and image rendererscreateWorkbook()builds buffered workbooks for small and medium exportscreateWorkbookStream()commits row batches for large exports with much flatter memory usage
The same schema works with both builders.
Quick Example
import { createExcelSchema, createWorkbook } from "typed-xlsx"; type Invoice = { id: string; qty: number; unitPrice: number }; const schema = createExcelSchema<Invoice>() .column("id", { header: "Invoice #", accessor: "id" }) .column("qty", { header: "Qty", accessor: "qty" }) .column("unitPrice", { header: "Unit Price", accessor: "unitPrice", style: { numFmt: "$#,##0.00" }, }) .column("total", { header: "Total", formula: ({ refs, fx }) => fx.round(refs.column("qty").mul(refs.column("unitPrice")), 2), style: { numFmt: "$#,##0.00" }, summary: (s) => [s.formula("sum")], }) .build(); const rows: Invoice[] = [{ id: "INV-001", qty: 3, unitPrice: 49.99 }]; const workbook = createWorkbook(); workbook.sheet("Invoices", { freezePane: { rows: 1 } }).table("invoices", { rows, schema, }); await workbook.writeToFile("./invoices.xlsx");
What Makes It Different
Type-safe schemas
Column accessors are verified against your row type. Dot-path accessors and callback accessors both preserve inference.
Formula DSL with typed references
refs.column("subtotal") is checked at definition time. If a formula references a column that does not exist yet, TypeScript fails before export.
One schema, two output paths
Use the exact same schema with:
createWorkbook()for buffered compositioncreateWorkbookStream()for async batch commits
Two schema modes
- report mode for summary rows, sub-row expansion, grouped headers, and custom report layouts
- excel-table mode for native Excel tables with autoFilter, totals rows, structured references, and table styles
Zero runtime dependencies
The package ships its own OOXML serializer and ZIP engine. No SheetJS. No ExcelJS.
For the detailed tradeoffs, feature matrix, and cases where the alternatives still win:
Real Workbook Outputs
Each example links to the full artifact in the playground.
Kitchen sink feature tour
Open one workbook with a feature map and one named sheet per major library primitive.
Example: Kitchen Sink Feature Tour
Board-ready multi-sheet reporting
Best for stakeholder and executive exports.
Runtime-driven column matrices
Generate columns from typed runtime context while formulas and totals stay readable.
Streaming exports at production scale
The schema stays the same while the builder switches to batch commits.
Product catalogs with media
Embed product thumbnails, use URL-backed image formulas, and combine badge, checkbox, hyperlink, and sparkline renderer columns.
Example: Product Media Catalog
Editable workflow-safe workbooks
Unlock inputs, validate user edits, and keep logic columns protected.
Nested records without manual row math
Array-valued accessors expand logical rows into multiple physical rows automatically.
Buffered And Streaming
Use buffered mode when the dataset is already in memory and the export size is moderate.
import { createWorkbook } from "typed-xlsx"; const workbook = createWorkbook(); workbook.sheet("Orders", { freezePane: { rows: 1 } }).table("orders", { rows, schema, }); await workbook.writeToFile("./orders.xlsx");
Use streaming mode when rows come from a cursor, paginated API, or a very large dataset.
import { createWorkbookStream } from "typed-xlsx"; const workbook = createWorkbookStream(); const table = await workbook.sheet("Orders", { freezePane: { rows: 1 } }).table("orders", { schema, }); for await (const batch of cursor) { await table.commit({ rows: batch }); } await workbook.writeToFile("./orders.xlsx");
Report Mode Vs Excel Table Mode
Use report mode when you need:
- summary rows
- sub-row expansion
- grouped headers
- more custom report layouts
Use excel-table mode when you need:
- native Excel filter and sort dropdowns
- totals rows
- structured references such as
[@Revenue] - true Excel table behavior for downstream spreadsheet workflows
import { createExcelSchema, createWorkbook } from "typed-xlsx"; const schema = createExcelSchema<{ units: number; revenue: number }>({ mode: "excel-table" }) .column("units", { header: "Units", accessor: "units", totalsRow: { function: "sum" }, }) .column("revenue", { header: "Revenue", accessor: "revenue", totalsRow: { function: "sum" }, style: { numFmt: '"$"#,##0.00' }, }) .column("avgPrice", { header: "Avg Price", formula: ({ refs, fx }) => fx.round(fx.safeDiv(refs.column("revenue"), refs.column("units")), 2), style: { numFmt: '"$"#,##0.00' }, totalsRow: { label: "-" }, }) .build(); createWorkbook() .sheet("Forecast") .table("forecast", { rows: [{ units: 42, revenue: 8400 }], schema, name: "ForecastTable", style: "TableStyleMedium2", autoFilter: true, totalsRow: true, });
Installation
pnpm add typed-xlsx
npm install typed-xlsx
yarn add typed-xlsx
bun add typed-xlsx
Start Here
- Introduction
- Quick Start
- Comparison with SheetJS and ExcelJS
- Schema Modes
- Buffered vs Streaming
- Excel Table Mode
- Streaming Overview
- Playground and example artifacts
License
MIT License © 2023-PRESENT Cyprien THAO




