Typed-xlsx
Schema Builder

Defining Columns

Column IDs, typed accessors, transforms, display options, and building a schema.

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
();
No overload matches this call. Overload 1 of 3, '(id: "name", definition: AccessorColumnInput<{ user: { name: string; }; }, "user" | "user.name", "name", never, never>): SchemaBuilder<{ user: { name: string; }; }, "name", never, {}>', gave the following error. Type '"user.typo"' is not assignable to type '"user" | "user.name"'. Overload 2 of 3, '(id: "name", definition: AccessorColumnInput<{ user: { name: string; }; }, (row: { user: { name: string; }; }) => unknown, "name", never, never>): SchemaBuilder<{ user: { name: string; }; }, "name", never, {}>', gave the following error. Type 'string' is not assignable to type '(row: { user: { name: string; }; }) => unknown'. Overload 3 of 3, '(id: "name", definition: FormulaColumnInput<{ user: { name: string; }; }, "name", never, never>): SchemaBuilder<{ user: { name: string; }; }, "name", never, {}>', gave the following error. Type 'string' is not assignable to type 'undefined'.

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

OptionTypeDescription
headerstring | (() => string)Column header label. Defaults to the column ID.
widthnumberFixed column width in characters.
minWidthnumberMinimum width — auto-sizing won't go below this.
maxWidthnumberMaximum width — auto-sizing won't exceed this.
autoWidthbooleanMeasure 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:

OptionMeaning
"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.

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
};
Copyright © 2026 Cyprien Thao. Released under the MIT License.