typed-xlsx
Getting Started

Introduction

typed-xlsx is a TypeScript-first Excel report builder with a type-safe schema DSL, formula columns, streaming exports, and a custom OOXML engine — no SheetJS, no wrappers.

typed-xlsx is a schema-driven XLSX builder for TypeScript. Define your report once as a typed schema — columns, formulas, styles, summaries — then pass rows to it. Get a spreadsheet back.

Everything is statically typed end-to-end: column accessors, formula references, schema context, and summary reducers. If your schema doesn't match your row type, TypeScript tells you before you run anything.

mermaid
flowchart LR
  S["Schema<br/><small>columns, formulas,<br/>styles, summaries</small>"]
  T["Table<br/><small>rows + schema +<br/>column selection</small>"]
  W["Workbook<br/><small>sheets, freeze panes,<br/>layout</small>"]
  O["Output<br/><small>Buffer · File ·<br/>Stream</small>"]

  S --> T --> W --> O

A real schema in 30 lines

import { 
createExcelSchema
,
createWorkbook
} from "typed-xlsx";
type
Invoice
= {
id
: string;
customer
: string;
qty
: number;
unitPrice
: number;
taxRate
: number;
status
: "paid" | "pending" | "overdue";
}; const
schema
=
createExcelSchema
<
Invoice
>()
.
column
("id", {
header
: "Invoice #",
accessor
: "id",
width
: 14 })
.
column
("customer", {
header
: "Customer",
accessor
: "customer",
minWidth
: 20 })
.
column
("qty", {
header
: "Qty",
accessor
: "qty",
width
: 8 })
.
column
("unitPrice", {
header
: "Unit Price",
accessor
: "unitPrice",
style
: {
numFmt
: "$#,##0.00" },
}) // Formula column — Excel evaluates this live; references are type-checked .
column
("subtotal", {
header
: "Subtotal",
formula
: ({
row
,
refs
,
fx
}) =>
fx
.
round
(
refs
.
column
("qty").
mul
(
refs
.
column
("unitPrice")), 2),
style
: {
numFmt
: "$#,##0.00" },
summary
: (
s
) => [
s
.
formula
("sum")], // SUM footer row
}) .
column
("taxRate", {
header
: "Tax %",
accessor
: "taxRate",
style
: {
numFmt
: "0%" } })
.
column
("total", {
header
: "Total",
formula
: ({
row
,
refs
,
fx
}) =>
fx
.
round
(
refs
.
column
("subtotal").
mul
(
refs
.
column
("taxRate").
add
(1)), 2),
style
: {
numFmt
: "$#,##0.00" },
summary
: (
s
) => [
s
.
formula
("sum")],
}) .
column
("status", {
header
: "Status",
accessor
: "status",
// Per-row conditional style — full type inference on `row`
style
: ({
row
}) => ({
font
: {
color
: {
rgb
:
row
.
status
=== "paid" ? "166534" :
row
.
status
=== "overdue" ? "B42318" : "92400E",
}, }, }), }) .
build
();
const
workbook
=
createWorkbook
();
workbook .
sheet
("Invoices", {
freezePane
: {
rows
: 1 } })
.
table
("invoices", {
rows
: [],
schema
,
title
: "Invoice Report — Q1 2025" });
await
workbook
.
writeToFile
("./invoices.xlsx");

The schema is a plain, stateless object. Pass it to as many tables as you need — different rows, different column selections, different summaries.

Why type-safety matters here

Most Excel libraries give you a row/cell API: ws['A1'] = { v: row.name }. Nothing stops you from writing row.nmae. There is no autocomplete on column IDs. There is no type error when a formula references a column that doesn't exist yet.

typed-xlsx builds the type state into the schema chain:

  • Accessors"user.address.city" dot-paths and (row) => row.value callbacks are fully typed against your row type T.
  • Formula referencesrefs.column("subtotal") is a compile error if subtotal hasn't been declared before this column. The predecessor constraint is enforced by the TypeScript type system at definition time.
  • Schema contextcreateExcelSchema<Row, Context>() declares a schema-wide runtime context object available to dynamic builders, formulas, styles, transforms, formats, hyperlinks, and other typed callbacks. Missing or wrong-typed context is a compile error.
  • Column selectionselect.include and select.exclude accept a union of your declared column IDs. No typos get through.

What the library includes

FeatureNotes
Type-safe schema builderColumn accessors, formulas, styles, summaries all typed against T
Formula DSLrefs.column, row.series, fx.round/if/abs/min/max, arithmetic, conditionals — emits live Excel formulas
Excel table modeNative <table> with autoFilter, styled banded rows, totals row, structured refs
Dynamic columnsGenerate columns from runtime data; schema context shape statically inferred
Reducer-based summariesinit / step / finalize accumulators, works in both buffered and streaming builds
Multi-table sheet layoutsMultiple tables per sheet, configurable column/row gaps
Sub-row expansionArray-valued accessors expand into multiple rows with cell merges
Streaming buildercreateWorkbookStream() — batch-commit API, bounded memory, supports all features
Custom OOXML + ZIP engineNo SheetJS, no third-party spreadsheet library in the call stack
Multiple output targetsBuffer, file, Node readable/writable, Web ReadableStream / WritableStream

Acknowledgment

The custom OOXML serializer and ZIP-based spreadsheet engine in typed-xlsx were heavily inspired by hucre and the work of productdevbook. That project showed me it was possible to build a custom OOXML serializer and zip it into a valid Excel file, which became the foundation for this library's spreadsheet engine.

Two builders, same schemas

import { 
createWorkbook
,
createWorkbookStream
} from "typed-xlsx";
declare const
rows
:
Array
<
Record
<string, unknown>>;
declare const
schema
: import("typed-xlsx").
SchemaDefinition
<
Record
<string, unknown>>;
declare const
cursor
:
AsyncIterable
<
Array
<
Record
<string, unknown>>>;
// Buffered — synchronous composition, full dataset in memory const
wb
=
createWorkbook
();
wb
.
sheet
("Report").
table
("report", {
rows
,
schema
});
const
buffer
=
wb
.
toBuffer
();
// Streaming — async commit-based, bounded memory const
stream
=
createWorkbookStream
();
const
table
= await
stream
.
sheet
("Report").
table
("report", {
schema
});
for await (const
batch
of
cursor
) {
await
table
.
commit
({
rows
:
batch
});
} await
stream
.
writeToFile
("./report.xlsx");

Both builders accept the same schema definitions. The only difference is the call pattern.

When to use typed-xlsx

ScenarioRecommendation
Complex reports from TypeScript data modelstyped-xlsx — typed schemas, formulas, summaries, styling
Native Excel tables with autoFilter and totalstyped-xlsx with mode: "excel-table"
Streaming exports of 100k+ rowstyped-xlsx with createWorkbookStream()
Reusing one schema across multiple export viewstyped-xlsx — schemas carry no state
Parsing, rewriting, or broader spreadsheet workflowshucre or another lower-level spreadsheet toolkit

Next steps

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