API: FormatSpec
A structured, thread-safe value-formatting description. Prefer FormatSpec on worker/stream paths; the actual reach of the function form is covered in "Function form" at the end.
Type definition
type FormatSpec =
| { type: "enum"; map: Record<string, string>; fallback?: string }
| { type: "date"; pattern?: string } // default "yyyy-MM-dd"
| { type: "datetime"; pattern?: string } // default "yyyy-MM-dd HH:mm"
| { type: "number"; decimals?: number; thousands?: boolean }
| { type: "padding"; fill: string; length: number; align?: "left" | "right" };Per type
enum
{ type: "enum", map: { paid: "Paid", pending: "Pending" }, fallback: "Unknown" }Outputs the mapped label; unmapped values use fallback, or pass through when absent.
date / datetime
{ type: "date" } // default yyyy-MM-dd
{ type: "datetime", pattern: "yyyy-MM-dd HH:mm:ss" }Accepts Date / parseable string / timestamp. The Workbook path writes an Excel date serial and auto-injects numFormat; the stream path outputs the pattern-formatted string. Values are interpreted by their UTC components (matching the workbook serial's dateToSerial convention, so all paths and timezones render the same); ISO date strings parse as UTC midnight per ECMA-262 — see the timezone note in Value Formatting. Pattern tokens differ between paths: the stream path lower-cases the pattern, parses only yyyy / MM / dd / HH / mm / ss, and emits anything else as-is — not interpreting quoted literals, so yyyy"年"M"月" renders 2026"年"m"月" — while the Workbook path renders any valid Excel format code, quoted literals included — see the note in Value Formatting.
number
{ type: "number", decimals: 2, thousands: true }decimals defaults to 0, thousands to false; decimals is validated as an integer in 0–100 (the toFixed ceiling the stream path relies on). Always set decimals explicitly: the Workbook path keeps full precision rendered via numFormat, while Stream/fallback paths bake decimals into the stored value — the two can differ otherwise.
Cross-path thousands: the Workbook path renders the separator via an auto-injected #,##0 numFormat; the stream path (>= 50,000 rows / degraded exports) cannot use numFormat and keeps the cell a number, so separators are not visible there (baking them into the value would turn data cells into text and break downstream calculations).
null/undefined values (and blank/whitespace-only strings — the most common missing-value shape coming out of databases, forms and CSV imports) render as empty cells on every path — never 0. padding treats them the same way: an empty cell, not a padded fake code like "00000".
padding
{ type: "padding", fill: "0", length: 6 } // "42" -> "000042"Omitting align (the default) maps to padStart: the fill goes on the left (value right-aligned) — right for leading-zero IDs. align: "left" maps to padEnd: the fill goes on the right (value left-aligned).
Function form
format: (value, row) => string | number | boolean;Can access the whole row for conditional formatting. Functions cannot cross the structured-clone boundary, so behavior differs per path:
- main path (auto mode: browser < 20,000 rows / Node < 50,000 rows; or any size with explicit
mode: "main"): executed normally; - Node's stream path (≥ 50,000 rows): also main-thread, executed normally;
- main-thread retry after a worker failure: executed normally — the original options keep the function, only the copy sent to the worker is stripped;
- browser worker path (auto ≥ 20,000 rows, or explicit
mode: "worker"/mode: "stream"): functions are stripped with aconsole.warnand the column exports its raw value (no error, no fallback to main).
Convert to FormatSpec to keep formatting on the worker path.