Defining Columns
A schema is built with createExcelSchema<T>(). Each .column(id, definition) call adds a column to the builder. Call .build() at the end to freeze the schema into a reusable SchemaDefinition.
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const schema = createExcelSchema<{
name: string;
email: string;
role: string;
}>()
.column("name", { header: "Name", accessor: "name" })
.column("email", { header: "Email", accessor: "email" })
.column("role", { header: "Role", accessor: "role" })
.build();
The first argument to .column() is the column ID — a stable string used for selection, formula references, and summary routing. It must be unique within the schema; duplicates throw immediately when the duplicate column or group is declared.
Typed path accessors
The simplest accessor is a dot-path string. typed-xlsx infers all valid paths from T — you get autocomplete and a type error if the path doesn't exist:
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const schema = createExcelSchema<{
user: { firstName: string; lastName: string };
company: { name: string; vatNumber: string };
}>()
.column("firstName", { accessor: "user.firstName" })
.column("lastName", { accessor: "user.lastName" })
.column("company", { accessor: "company.name" })
.column("vat", { accessor: "company.vatNumber" })
.build();
import { createExcelSchema } from "@chronicstone/typed-xlsx";
// Path 'user.typo' doesn't exist on the type — TypeScript rejects it
createExcelSchema<{ user: { name: string } }>().column("name", { accessor: "user.typo" }).build();
Callback accessors
Use a callback when the cell value is derived from multiple fields or needs transformation at extraction time:
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const schema = createExcelSchema<{
firstName: string;
lastName: string;
hiredAt: string;
salary: number;
}>()
.column("fullName", {
header: "Employee",
accessor: (row) => `${row.firstName} ${row.lastName}`,
})
.column("tenure", {
header: "Years",
accessor: (row) => {
const hired = new Date(row.hiredAt);
return Math.floor((Date.now() - hired.getTime()) / (1000 * 60 * 60 * 24 * 365));
},
})
.build();
Transform
transform runs after the accessor. It receives the extracted value (fully typed) and returns what goes into the cell:
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const schema = createExcelSchema<{
status: "active" | "inactive" | "suspended";
planCode: "starter" | "pro" | "enterprise";
}>()
.column("status", {
accessor: "status",
transform: (value) => value.charAt(0).toUpperCase() + value.slice(1),
})
.column("plan", {
accessor: "planCode",
transform: (value) => ({ starter: "Starter", pro: "Pro", enterprise: "Enterprise" })[value],
})
.build();
Default value
defaultValue is used when the accessor returns null or undefined:
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const schema = createExcelSchema<{
name: string;
department: string | null;
}>()
.column("name", { accessor: "name" })
.column("department", {
accessor: "department",
defaultValue: "Unassigned",
})
.build();
Column display options
| Option | Type | Description |
|---|---|---|
header | string | (() => string) | Column header label. Defaults to the column ID. |
width | number | Fixed column width in characters. |
minWidth | number | Minimum width — auto-sizing won't go below this. |
maxWidth | number | Maximum width — auto-sizing won't exceed this. |
autoWidth | boolean | Measure cell content and set width automatically. |
autoWidth and width can be combined: autoWidth measures content, then minWidth and maxWidth clamp the result. A fixed width overrides auto-sizing entirely.
Formula expansion
Formula columns also support an expansion option:
| Option | Meaning |
|---|---|
"auto" | Infer whether the formula should repeat per physical row or collapse to one logical-row result. |
"single" | Render one value for the logical row and merge the remaining physical rows in that column. |
"expand" | Render the formula on every physical row produced by the logical row. |
This is only relevant in report mode when at least one column expands a logical row into multiple physical rows.
Display options example
import { createExcelSchema } from "@chronicstone/typed-xlsx";
const schema = createExcelSchema<{ description: string; amount: number }>()
.column("description", {
accessor: "description",
autoWidth: true,
minWidth: 20,
maxWidth: 60,
})
.column("amount", {
accessor: "amount",
width: 14,
style: { numFmt: "$#,##0.00", alignment: { horizontal: "right" } },
})
.build();
Data validation
Columns support native Excel data validation via the validation field. See Data Validation for the full walkthrough.
Hyperlinks
Columns support native Excel hyperlinks via the hyperlink field. See Hyperlinks for the full walkthrough.
Multi-value cells (sub-row expansion)
If an accessor or transform returns an array, typed-xlsx expands the logical row into multiple physical rows and merges single-value cells automatically. This is a report-mode only feature.
import { createExcelSchema } from "@chronicstone/typed-xlsx";
// One subscription → multiple billing line items in the export
const schema = createExcelSchema<{
subscriptionId: string;
customer: string;
lineItems: Array<{ description: string; amount: number }>;
}>()
.column("subscriptionId", { header: "Sub ID", accessor: "subscriptionId" })
.column("customer", { accessor: "customer" })
.column("lineDescription", {
header: "Line Item",
accessor: (row) => row.lineItems.map((l) => l.description),
})
.column("lineAmount", {
header: "Amount",
accessor: (row) => row.lineItems.map((l) => l.amount),
style: { numFmt: "$#,##0.00" },
})
.build();
Calling .build()
.build() freezes the schema and returns a SchemaDefinition. A schema can be built once and reused across any number of workbooks, sheets, or threads.
import { createExcelSchema } from "@chronicstone/typed-xlsx";
// Build once, reuse everywhere
const productSchema = createExcelSchema<{ sku: string; name: string; stock: number }>()
.column("sku", { header: "SKU", accessor: "sku" })
.column("name", { header: "Product", accessor: "name" })
.column("stock", { header: "In Stock", accessor: "stock" })
.build();
// productSchema is a plain frozen object — safe to export and import across modules
export { productSchema };