typed-xlsx
Migration

v0 → v1

Every breaking change from v0.x to v1.0 with before/after examples.

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.

v0v1
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" }).


keyaccessor

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" })

labelheader

// v0
.column("name", { key: "name", label: "Full Name" })

// v1
.column("name", { accessor: "name", header: "Full Name" })

defaultdefaultValue

// v0
.column("score", { key: "score", default: 0 })

// v1
.column("score", { accessor: "score", defaultValue: 0 })

cellStylestyle

// v0
.column("amount", {
  key: "amount",
  cellStyle: { numFmt: "$#,##0.00" },
})

// v1
.column("amount", {
  accessor: "amount",
  style: { numFmt: "$#,##0.00" },
})

headerStyle is unchanged.


CellStyle shape — fill.fgColorfill.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 / methodv1 equivalent
.addTable({ ... }).table("table-id", { ... })
data: T[]rows: T[]
titleStyleremoved — title is a plain string only
summary?: booleanremoved — 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

v0v1
tablesPerRowtablesPerRow (unchanged)
tableSeparatorWidthtableColumnGap
rtl on .build()rightToLeft on .sheet()
rowHeightremoved
extraLengthtableColumnGap
// 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 exportStatus in v1
ExcelSchemaBuilderRemoved — use createExcelSchema<T>()
ExcelBuilderRemoved — use createWorkbook()
TransformersMapRemoved — no longer needed
FormattersMapRemoved — no longer needed
FormatterPresetRemoved — no longer needed
TOutputTypeRemoved — output is via explicit methods
ExcelBuildParamsRemoved
ExcelBuildOutputRemoved
SheetTableBuilderRemoved

CellStyle is still exported. Its shape changed (see above).


Quick reference

Changev0v1
Schema factoryExcelSchemaBuilder.create<T>()createExcelSchema<T>()
Workbook factoryExcelBuilder.create()createWorkbook()
Column field pathkeyaccessor
Column headerlabelheader
Column fallbackdefaultdefaultValue
Column stylecellStylestyle
Fill colorfill.fgColor.rgbfill.color.rgb
Transform signature(value, rowIndex)({ value, row, rowIndex, ctx })
Row array fielddatarows
Add table method.addTable().table()
Column selection{ col: true/false }{ include: [] } / { exclude: [] }
Summary reducervalue: (data) => ...init / step / finalize
Summary stylecellStylestyle
Table title styletitleStyleremoved
Summary togglesummary: boolean on tableremoved — implicit when columns define summaries
Sheet column gaptableSeparatorWidth / extraLengthtableColumnGap
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)
Copyright © 2026 Cyprien Thao. Released under the MIT License.