Skip to content

Spreadsheets, read and written honestly ​

A TypeScript-first library for .xlsx and CSV. The buffered path is synchronous and speaks Uint8Array, so writing a workbook is a function call that hands you bytes, and reading one is a function call that hands you a model. One runtime dependency, and every behaviour pinned by a regression case.

shell
npm install @shbernal/ts-xlsx
import {readXlsx, Workbook, writeXlsx} from '@shbernal/ts-xlsx';

const workbook = new Workbook();
const sheet = workbook.addWorksheet('People');
sheet.addRow(['Name', 'Joined']);
sheet.addRow(['Ada', new Date('2026-01-01')]);

const bytes: Uint8Array = writeXlsx(workbook);
console.log(readXlsx(bytes).requireWorksheet('People').getCell('A2').value); // 'Ada'

The dependency tree

1 runtime dependency ​

fflate, for zip. Everything else is written here: the XML reader, the OOXML model, the CSV codec, the compound-file reader that opens a macro project. A large part of why this library exists is that the project it forked from had a rotting transitive tree, so the audit is part of CI and is expected to stay green.

Types

The types are the contract ​

strict, plus noUncheckedIndexedAccess and exactOptionalPropertyTypes. The API reference is generated straight from the published declarations, so it cannot describe a shape the compiler would reject, and a wrong page in it is a wrong JSDoc comment in the source rather than a documentation task (ADR-0006).

A cell's value is one precisely typed union rather than a type field you set alongside a value. What you assign is what the cell is.

Correctness

357 regression cases, harvested from a real backlog ​

This library is a hard fork of ExcelJS, and the fork's first job was to get the knowledge out before discarding the code. Every credible bug, reproduction and edge case in that backlog became a corpus case written against behaviour rather than against an implementation, which is how they outlived the rewrite they were built to survive. A bug without a case is a bug that will return.

Beside them sit 160 hand-authored spec notes, the evidence a case is written from, and 42 decision records saying why each fork in the road went the way it did, including the ones that were later retracted.

Output

Emitted files are validated, not assumed ​

"It opens in Excel" is not a test. Generated packages are checked against Microsoft's own OpenXmlValidator, schema and semantics both, as an independent oracle rather than as this library grading its own homework (ADR-0002).

The bytes are also a pure function of the model: an unchanged workbook written twice produces two identical archives, because entry timestamps are pinned rather than clocked (ADR-0032). A committed .xlsx therefore changes only when something about it changed.

The honest part

Three states, and there is no fourth ​

Every part of a workbook this library meets is in exactly one of them.

Modelled

The reader understands it and the writer can rebuild it. Cells, styles, formulas, tables, merges, panes, validation, conditional formats, comments, images, page setup.

Preserved

The model does not interpret it, and the bytes cross a load and save untouched. Pivot caches, slicers, charts, shapes, linked-workbook references, a VBA project.

Refused

The library says so, with a typed error naming which of four kinds of failure it was, rather than guessing and handing back something plausible.

There is deliberately no "approximated" state, and that absence is the point: a library that silently half-understands a part is one whose output you cannot trust without opening it.

Say the limit in the same breath as the claim. Charts, vector shapes, slicers and legacy form controls are in the preserved column, not the modelled one: a workbook that has them keeps them, and there is no API to author a new one (ADR-0014). If that is what you came for, you want a different library, and it is better that you learn it here than after adopting this one.

Untrusted input

Every parser path assumes the file is hostile ​

Reading a spreadsheet means running someone else's bytes through your process. Inflation is bounded by counting the output actually produced, never by trusting the archive's declared sizes, so a zip bomb that lies about its size is refused all the same. XML entities are decoded but never expanded. Neither defence is configurable, and neither can be switched off.

See for yourself

The playground is the claim, running ​

The playground writes a workbook, reads it back, round-trips it, and shows you the emitted package part by part, in your own tab, with no server behind it. Drop a spreadsheet of your own and you are testing the reader against a producer with its own habits, which is where a spreadsheet library actually gets hard.

Released under the MIT License.