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 "@chronicstone/typed-xlsx";

const schema = ExcelSchemaBuilder.create<Row>().column(...).build();
const workbook = ExcelBuilder.create();

// v1
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";

const schema = createExcelSchema<Row>().column(...).build();
const workbook = createWorkbook();

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 — value, full row, rowIndex
transform: (value, row, rowIndex) => 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: (v) => upper(v) })
  .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({
  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({ ... })
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({
  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 column groups

The context type is now explicit in the second parameter of the .group() callback. Context is still injected at table creation time via the context field keyed by group id.

// 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>()
  .group("dates", (builder, ctx: Record<string, string>) => {
    for (const [key, label] of Object.entries(ctx))
      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 Stream Workbook 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)
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.