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 package has been renamed to xlsmith.
npm install xlsmith// before
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
// after
import { createExcelSchema, createWorkbook } from "xlsmith";- 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
- Full comparison against SheetJS and ExcelJS: Why xlsmith?
Most XLSX libraries give you a cell API. xlsmith 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, and conditional styles
If you are currently evaluating xlsmith against lower-level spreadsheet libraries, read the full comparison with SheetJS and ExcelJS:
Choose xlsmith when your main job is generating typed reports from application data, not manually editing spreadsheets cell by cell.
xlsmith: 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:
xlsmith 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
Reach for a lower-level spreadsheet library when you need to:
- read or modify existing
.xlsxfiles - embed charts or worksheet images
- support spreadsheet formats beyond
.xlsx - do highly manual cell-by-cell spreadsheet editing
Three functions cover the main surface:
createExcelSchema()describes columns, formulas, summaries, styles, groups, and validationcreateWorkbook()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.
import { createExcelSchema, createWorkbook } from "xlsmith";
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");Column accessors are verified against your row type. Dot-path accessors and callback accessors both preserve inference.
refs.column("subtotal") is checked at definition time. If a formula references a column that does not exist yet, TypeScript fails before export.
Use the exact same schema with:
createWorkbook()for buffered compositioncreateWorkbookStream()for async batch commits
- 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
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:
Each screenshot links to the full artifact in the playground.
Best for stakeholder and executive exports.
Generate columns from typed runtime context while formulas and totals stay readable.
The schema stays the same while the builder switches to batch commits.
Unlock inputs, validate user edits, and keep logic columns protected.
Array-valued accessors expand logical rows into multiple physical rows automatically.
Use buffered mode when the dataset is already in memory and the export size is moderate.
import { createWorkbook } from "xlsmith";
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 "xlsmith";
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");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 "xlsmith";
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,
});pnpm add xlsmithnpm install xlsmithyarn add xlsmithbun add xlsmith- Introduction
- Quick Start
- Comparison with SheetJS and ExcelJS
- Schema Modes
- Buffered vs Streaming
- Excel Table Mode
- Streaming Overview
- Playground and example artifacts
MIT License © 2023-PRESENT Cyprien THAO




