Typed-xlsx
Getting Started

Quick Start

Build your first XLSX report — buffered or streaming — with typed schemas, formula columns, and summary rows.

This guide walks you through a complete invoice report with formula columns, a summary row, and conditional styling. Once the schema is built you'll see how the same definition works for both buffered and streaming output.

Install

npm install @chronicstone/typed-xlsx

Define your row type

type Invoice = {
  invoiceId: string;
  customer: string;
  issueDate: string;
  qty: number;
  unitPrice: number;
  taxRate: number;
  status: "paid" | "pending" | "overdue";
};

Build the schema

type Invoice = {
  invoiceId: string;
  customer: string;
  issueDate: string;
  qty: number;
  unitPrice: number;
  taxRate: number;
  status: "paid" | "pending" | "overdue";
};
// ---cut---
import { createExcelSchema } from "@chronicstone/typed-xlsx";

const invoiceSchema = createExcelSchema<Invoice>()
  .column("invoiceId", { header: "Invoice #", accessor: "invoiceId", width: 14 })
  .column("customer", { header: "Customer", accessor: "customer", minWidth: 20 })
  .column("issueDate", { header: "Issue Date", accessor: "issueDate", width: 14 })
  .column("qty", { header: "Qty", accessor: "qty", width: 8 })
  .column("unitPrice", {
    header: "Unit Price",
    accessor: "unitPrice",
    width: 14,
    style: { numFmt: "$#,##0.00" },
  })
  // Formula column: Qty * Unit Price, rounded to 2 dp
  .column("subtotal", {
    header: "Subtotal",
    formula: ({ row, fx }) => fx.round(row.ref("qty").mul(row.ref("unitPrice")), 2),
    width: 14,
    style: { numFmt: "$#,##0.00" },
    summary: (s) => [s.formula("sum")],
  })
  .column("taxRate", {
    header: "Tax %",
    accessor: "taxRate",
    width: 10,
    style: { numFmt: "0.0%" },
  })
  .column("taxAmount", {
    header: "Tax",
    formula: ({ row, fx }) => fx.round(row.ref("subtotal").mul(row.ref("taxRate")), 2),
    width: 12,
    style: { numFmt: "$#,##0.00" },
    summary: (s) => [s.formula("sum")],
  })
  .column("total", {
    header: "Total",
    formula: ({ row }) => row.ref("subtotal").add(row.ref("taxAmount")),
    width: 14,
    style: { numFmt: "$#,##0.00" },
    summary: (s) => [s.formula("sum")],
  })
  .column("status", {
    header: "Status",
    accessor: "status",
    width: 12,
    style: (row) => ({
      font: {
        bold: row.status === "overdue",
        color: {
          rgb: row.status === "paid" ? "166534" : row.status === "overdue" ? "B42318" : "92400E",
        },
      },
    }),
  })
  .build();

The schema is a plain, stateless object. It works identically in buffered and streaming mode.

Buffered output

Best for small-to-medium datasets (up to ~50 k rows) where you already have all rows in memory:

type Invoice = {
  invoiceId: string;
  customer: string;
  issueDate: string;
  qty: number;
  unitPrice: number;
  taxRate: number;
  status: "paid" | "pending" | "overdue";
};
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const invoiceSchema = createExcelSchema<Invoice>()
  .column("invoiceId", { accessor: "invoiceId" })
  .column("customer", { accessor: "customer" })
  .column("issueDate", { accessor: "issueDate" })
  .column("qty", { accessor: "qty" })
  .column("unitPrice", { accessor: "unitPrice" })
  .column("subtotal", {
    formula: ({ row, fx }) => fx.round(row.ref("qty").mul(row.ref("unitPrice")), 2),
  })
  .column("taxRate", { accessor: "taxRate" })
  .column("taxAmount", {
    formula: ({ row, fx }) => fx.round(row.ref("subtotal").mul(row.ref("taxRate")), 2),
  })
  .column("total", { formula: ({ row }) => row.ref("subtotal").add(row.ref("taxAmount")) })
  .column("status", { accessor: "status" })
  .build();
// ---cut---
import { createWorkbook } from "@chronicstone/typed-xlsx";

const rows: Invoice[] = [
  {
    invoiceId: "INV-001",
    customer: "Acme Corp",
    issueDate: "2025-01-15",
    qty: 10,
    unitPrice: 299,
    taxRate: 0.2,
    status: "paid",
  },
  {
    invoiceId: "INV-002",
    customer: "Globex Inc",
    issueDate: "2025-01-22",
    qty: 5,
    unitPrice: 850,
    taxRate: 0.2,
    status: "pending",
  },
  {
    invoiceId: "INV-003",
    customer: "Initech",
    issueDate: "2025-02-03",
    qty: 2,
    unitPrice: 1200,
    taxRate: 0.2,
    status: "overdue",
  },
];

const workbook = createWorkbook();
workbook.sheet("Invoices", { freezePane: { rows: 1 } }).table("invoices", {
  schema: invoiceSchema,
  rows,
  title: "Invoice Report — Q1 2025",
});

await workbook.writeToFile("./invoices.xlsx");

// Or get a Buffer for HTTP responses, S3 uploads, etc.
const buffer = workbook.toBuffer();

Streaming output

For large datasets (100 k+ rows), database cursors, or paginated APIs. The schema is unchanged — only the builder call pattern differs:

type Invoice = {
  invoiceId: string;
  customer: string;
  issueDate: string;
  qty: number;
  unitPrice: number;
  taxRate: number;
  status: "paid" | "pending" | "overdue";
};
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const invoiceSchema = createExcelSchema<Invoice>()
  .column("invoiceId", { accessor: "invoiceId" })
  .column("customer", { accessor: "customer" })
  .column("issueDate", { accessor: "issueDate" })
  .column("qty", { accessor: "qty" })
  .column("unitPrice", { accessor: "unitPrice" })
  .column("subtotal", {
    formula: ({ row, fx }) => fx.round(row.ref("qty").mul(row.ref("unitPrice")), 2),
  })
  .column("taxRate", { accessor: "taxRate" })
  .column("taxAmount", {
    formula: ({ row, fx }) => fx.round(row.ref("subtotal").mul(row.ref("taxRate")), 2),
  })
  .column("total", { formula: ({ row }) => row.ref("subtotal").add(row.ref("taxAmount")) })
  .column("status", { accessor: "status" })
  .build();
// ---cut---
import { createWorkbookStream } from "@chronicstone/typed-xlsx";

const workbook = createWorkbookStream();

const table = await workbook
  .sheet("Invoices", { freezePane: { rows: 1 } })
  .table("invoices", { schema: invoiceSchema });

// Replace with your database cursor or paginated API
async function* fetchPages(): AsyncGenerator<Invoice[]> {
  yield [
    {
      invoiceId: "INV-001",
      customer: "Acme Corp",
      issueDate: "2025-01-15",
      qty: 10,
      unitPrice: 299,
      taxRate: 0.2,
      status: "paid",
    },
    {
      invoiceId: "INV-002",
      customer: "Globex Inc",
      issueDate: "2025-01-22",
      qty: 5,
      unitPrice: 850,
      taxRate: 0.2,
      status: "pending",
    },
  ];
}

for await (const page of fetchPages()) {
  await table.commit({ rows: page });
  // Memory freed after each commit — heap stays flat
}

await workbook.writeToFile("./invoices.xlsx");

Streaming supports all buffered features: formula columns, excel-table mode, groups, summaries, sub-row expansion, and freeze panes. See Streaming for output targets and memory tuning.

What the exported file includes

  • Formula columns that Excel evaluates live (=ROUND(D2*E2,2), etc.)
  • A summary row with SUM formulas for subtotal, tax, and total
  • Conditional text colour on the status column (green / amber / red)
  • A frozen header row for scroll stability

Next steps

Copyright © 2026 Cyprien Thao. Released under the MIT License.