v0 → v1
v1 is a complete ground-up rewrite. The xlsx-js-style / SheetJS dependency is gone. Every layer from schema definition to OOXML serialization to ZIP packaging has been rebuilt from scratch. This page covers every breaking change with before/after examples.
Entry point renames
The class-based static constructors are replaced with plain functions.
| v0 | v1 |
|---|---|
ExcelSchemaBuilder.create<T>() | createExcelSchema<T>() |
ExcelBuilder.create() | createWorkbook() |
// v0
import { ExcelSchemaBuilder, ExcelBuilder } from "typed-xlsx";
const schema = ExcelSchemaBuilder.create<Row>().column(...).build();
const workbook = ExcelBuilder.create();
// v1
import { createExcelSchema, createWorkbook } from "typed-xlsx";
const schema = createExcelSchema<Row>().column(...).build();
const workbook = createWorkbook();
createExcelSchema() now defaults to report mode. Native Excel table schemas use createExcelSchema<T>({ mode: "excel-table" }).
key → accessor
key accepted only a typed dot-path string — a column's value was always a single field. Any column that needed a computed or multi-field value required a separate transform step.
accessor accepts the same typed dot-path string or a function, at the same definition site. The function form removes the need for transform in the majority of cases.
// v0 — path only, computed values need a separate transform
ExcelSchemaBuilder.create<User>().column("fullName", {
key: "firstName",
transform: (val, row) => `${row.firstName} ${row.lastName}`,
});
// v1 — path or function, unified at the accessor
createExcelSchema<User>().column("fullName", {
accessor: (row) => `${row.firstName} ${row.lastName}`,
header: "Full Name",
});
Path access works exactly as before:
// v0
.column("city", { key: "address.city" })
// v1
.column("city", { accessor: "address.city" })
label → header
// v0
.column("name", { key: "name", label: "Full Name" })
// v1
.column("name", { accessor: "name", header: "Full Name" })
default → defaultValue
// v0
.column("score", { key: "score", default: 0 })
// v1
.column("score", { accessor: "score", defaultValue: 0 })
cellStyle → style
// v0
.column("amount", {
key: "amount",
cellStyle: { numFmt: "$#,##0.00" },
})
// v1
.column("amount", {
accessor: "amount",
style: { numFmt: "$#,##0.00" },
})
headerStyle is unchanged.
CellStyle shape — fill.fgColor → fill.color
v0 used xlsx-js-style's internal style shape. v1 defines and owns its CellStyle type. The only structural change is the fill color key.
// v0 (xlsx-js-style shape)
cellStyle: {
fill: { fgColor: { rgb: "FF0000" } },
font: { bold: true },
}
// v1 (CellStyle)
style: {
fill: { color: { rgb: "FF0000" } },
font: { bold: true },
}
All other style fields (font, border, alignment, numFmt) have the same shape.
transform — signature extended
The transform function gained access to the full row as its second argument. Update any transform that needed row context but was working around the limitation.
// v0 — only the extracted value, no row access
transform: (value, rowIndex) => value.toUpperCase();
// v1 — object callback with value, full row, rowIndex, and schema context
transform: ({ value, row, rowIndex, ctx }) => value.toUpperCase();
withTransformers() and withFormatters() removed
Named transformer and formatter registries are gone. Pass the function directly in the column definition, or extract it as a plain TypeScript constant.
// v0
ExcelSchemaBuilder.create<Row>()
.withTransformers({ upper: (v: string) => v.toUpperCase() })
.column("name", { key: "name", transform: "upper" })
.build();
// v1
const upper = (v: string) => v.toUpperCase();
createExcelSchema<Row>()
.column("name", { accessor: "name", transform: ({ value }) => upper(value) })
.build();
Summary API — reducer model
The v0 value function received the full T[] array. This required all rows to be resident at once, making streaming impossible.
v1 uses a three-function reducer (init / step / finalize) that the engine drives row-by-row. This is what enables summary support in stream mode. The cellStyle field on the summary definition is also renamed to style.
// v0
.column("balance", {
key: "balance",
summary: [
{
value: data => data.reduce((acc, row) => acc + row.balance, 0),
format: '"$"#,##0.00',
cellStyle: { font: { bold: true } },
},
{
value: data => data.reduce((acc, row) => acc + row.balance, 0) * 1.2,
format: '"$"#,##0.00',
},
],
})
// v1
.column("balance", {
accessor: "balance",
summary: (summary) => [
summary.cell({
init: () => 0,
step: (acc, row) => acc + row.balance,
finalize: acc => acc,
format: '"$"#,##0.00',
style: { font: { bold: true } },
}),
summary.cell({
init: () => 0,
step: (acc, row) => acc + row.balance,
finalize: acc => acc * 1.2,
format: '"$"#,##0.00',
}),
],
})
If you need a label column, use helpers like summary.label("Total"). Raw SummaryDefinition objects and arrays are still accepted, but the callback-builder form is the preferred public API.
Column selection — include / exclude
v0 used a per-column boolean map. Every column in the schema needed a key in the object, set to true or false.
v1 uses explicit include or exclude arrays. Specify only what you want to show or hide.
// v0 — must enumerate every column
.addTable({
schema,
data: rows,
select: { firstName: true, lastName: true, email: true, internalId: false },
})
// v1 — state only what you care about
.table("users", {
schema,
rows,
select: { include: ["firstName", "lastName", "email"] },
// or equivalently:
// select: { exclude: ["internalId"] },
})
Table input — field renames and method rename
| v0 field / method | v1 equivalent |
|---|---|
.addTable({ ... }) | .table("table-id", { ... }) |
data: T[] | rows: T[] |
titleStyle | removed — title is a plain string only |
summary?: boolean | removed — summaries render when defined on columns |
// v0
workbook.sheet("Report").addTable({
schema,
data: rows,
title: "Monthly Report",
titleStyle: { font: { bold: true } },
summary: true,
});
// v1
workbook.sheet("Report").table("report", {
schema,
rows,
title: "Monthly Report", // plain string only, no titleStyle
});
Sheet options
| v0 | v1 |
|---|---|
tablesPerRow | tablesPerRow (unchanged) |
tableSeparatorWidth | tableColumnGap |
rtl on .build() | rightToLeft on .sheet() |
rowHeight | removed |
extraLength | tableColumnGap |
// v0
ExcelBuilder.create()
.sheet("Report", { tablesPerRow: 2, tableSeparatorWidth: 2 })
.addTable(...)
.build({ output: "buffer", rtl: true })
// v1
createWorkbook()
.sheet("Report", { tablesPerRow: 2, tableColumnGap: 2, rightToLeft: true })
.table(...)
.toBuffer()
Output methods
v0 .build() required an output type parameter and returned the result synchronously with a type that depended on that parameter.
v1 separates output into explicit terminal methods on the workbook.
// v0
const buffer = workbook.build({ output: "buffer" });
const uint8 = workbook.build({ output: "uint8array" });
await workbook.build({ output: "file", filePath: "./report.xlsx" });
// v1
const buffer = workbook.toBuffer();
const uint8 = workbook.toUint8Array();
await workbook.writeToFile("./report.xlsx");
Dynamic columns
The context type is now explicit in the callback for runtime-generated columns. Context is still injected at table creation time via the context field.
In v1, groups are also part of the typed formula scope:
- formulas inside a group can reference previous outer columns
- later formulas can aggregate a previous group with scope helpers
// v0
schema.group("dates", (builder, ctx: Record<string, string>) => {
for (const [key, label] of Object.entries(ctx))
builder.column(key, { key: () => ..., label })
})
// v1
createExcelSchema<Row, { dates: Record<string, string> }>()
.dynamic("dates", (builder, { ctx }) => {
for (const [key, label] of Object.entries(ctx.dates))
builder.column(key, { accessor: () => ..., header: label })
})
.build()
Stream workbook
createWorkbookStream() is new in v1. There is no equivalent in v0 — the old streaming: true flag on .build() still materialized the full workbook before writing. See the Streaming Intro section for full documentation.
Removed exports
| v0 export | Status in v1 |
|---|---|
ExcelSchemaBuilder | Removed — use createExcelSchema<T>() |
ExcelBuilder | Removed — use createWorkbook() |
TransformersMap | Removed — no longer needed |
FormattersMap | Removed — no longer needed |
FormatterPreset | Removed — no longer needed |
TOutputType | Removed — output is via explicit methods |
ExcelBuildParams | Removed |
ExcelBuildOutput | Removed |
SheetTableBuilder | Removed |
CellStyle is still exported. Its shape changed (see above).
Quick reference
| Change | v0 | v1 |
|---|---|---|
| Schema factory | ExcelSchemaBuilder.create<T>() | createExcelSchema<T>() |
| Workbook factory | ExcelBuilder.create() | createWorkbook() |
| Column field path | key | accessor |
| Column header | label | header |
| Column fallback | default | defaultValue |
| Column style | cellStyle | style |
| Fill color | fill.fgColor.rgb | fill.color.rgb |
| Transform signature | (value, rowIndex) | ({ value, row, rowIndex, ctx }) |
| Row array field | data | rows |
| Add table method | .addTable() | .table() |
| Column selection | { col: true/false } | { include: [] } / { exclude: [] } |
| Summary reducer | value: (data) => ... | init / step / finalize |
| Summary style | cellStyle | style |
| Table title style | titleStyle | removed |
| Summary toggle | summary: boolean on table | removed — implicit when columns define summaries |
| Sheet column gap | tableSeparatorWidth / extraLength | tableColumnGap |
| RTL option | .build({ rtl }) | .sheet(name, { rightToLeft }) |
| Buffer output | .build({ output: "buffer" }) | .toBuffer() |
| Uint8Array output | .build({ output: "uint8array" }) | .toUint8Array() |
| File output | .build({ output: "file", filePath }) | .writeToFile(path) |