Column Groups
Groups let you generate columns programmatically from runtime data. The most common use case is one column per dynamic dimension — one column per organization, one column per month, one column per category.
Defining a group
.group(id, buildFn) accepts a builder callback that receives the schema builder and, optionally, a typed context value as its second parameter.
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
type User = {
firstName: string;
organizations: Array<{ id: number; name: string }>;
};
const schema = createExcelSchema<User>()
.column("firstName", { accessor: "firstName", header: "Name" })
.group("orgs", (builder, orgs: Array<{ id: number; name: string }>) => {
for (const org of orgs) {
builder.column(`org-${org.id}`, {
header: org.name,
accessor: (row) => (row.organizations.some((o) => o.id === org.id) ? "Yes" : "No"),
});
}
})
.build();
Injecting context at table time
Groups receive their context when the table is built, not when the schema is defined. Pass context via the context option on .table(), keyed by group id.
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
type User = {
firstName: string;
organizations: Array<{ id: number; name: string }>;
};
const schema = createExcelSchema<User>()
.column("firstName", { accessor: "firstName", header: "Name" })
.group("orgs", (builder, orgs: Array<{ id: number; name: string }>) => {
for (const org of orgs) {
builder.column(`org-${org.id}`, {
header: org.name,
accessor: (row) => (row.organizations.some((o) => o.id === org.id) ? "✓" : ""),
style: (row) => ({
fill: {
color: {
rgb: row.organizations.some((o) => o.id === org.id) ? "DCFCE7" : "FEF2F2",
},
},
alignment: { horizontal: "center" },
}),
});
}
})
.build();
const orgs = [
{ id: 1, name: "Acme Corp" },
{ id: 2, name: "Globex" },
{ id: 3, name: "Initech" },
];
const users: User[] = [];
createWorkbook().sheet("Users").table({
rows: users,
schema,
context: { orgs },
});
The schema definition carries no reference to orgs. The columns are generated fresh from whatever context is passed at table-build time.
Context shape is statically verified
The context object is fully typed. Hover over RequiredContext to see the exact shape the schema expects:
import { createExcelSchema } from "@chronicstone/typed-xlsx";
import type { SchemaGroupContext } from "@chronicstone/typed-xlsx";
type User = { name: string; orgs: Array<{ id: number; name: string }> };
const schema = createExcelSchema<User>()
.column("name", { accessor: "name" })
.group("orgs", (b, items: Array<{ id: number; name: string }>) => {
for (const item of items) {
b.column(`org-${item.id}`, {
header: item.name,
accessor: (r) => (r.orgs.some((o) => o.id === item.id) ? "Yes" : "No"),
});
}
})
.build();
type RequiredContext = SchemaGroupContext<typeof schema>;
The type is derived automatically from the second parameter of the group callback. Annotate that parameter and the context object shape is inferred without any extra helper type.
Missing context is a compile error when the group is selected
When a selected group declares a context parameter, context becomes a required property on .table(). Omitting it is caught at compile time, not at runtime:
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
type User = { name: string; orgs: number[] };
const schema = createExcelSchema<User>()
.column("name", { accessor: "name" })
.group("orgIds", (b, ids: number[]) => {
for (const id of ids) b.column(`org-${id}`, { accessor: (r) => r.orgs.includes(id) });
})
.build();
// Error: 'context' is required because the selected group needs it
createWorkbook()
.sheet("Report")
.table({
rows: [],
schema,
select: { include: ["orgIds"] },
});
Conversely, groups without a context parameter do not require context, and contextful groups can be excluded via select so context becomes optional again.
Wrong context type is caught
Supplying a context value of the wrong type is also a compile error:
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
type User = { name: string; orgs: number[] };
const schema = createExcelSchema<User>()
.column("name", { accessor: "name" })
.group("orgIds", (b, ids: number[]) => {
for (const id of ids) b.column(`org-${id}`, { accessor: (r) => r.orgs.includes(id) });
})
.build();
createWorkbook()
.sheet("Report")
.table({
rows: [],
schema,
context: {
orgIds: "should-be-a-number-array", // Error: string is not assignable to number[]
},
});
Use with column selection
Selection works at the group level. That means select.include and select.exclude accept the group id itself, not the ids of the generated child columns. This keeps the public API stable even when the group generates different columns depending on runtime context.
import { createExcelSchema, createWorkbook } from "@chronicstone/typed-xlsx";
type Row = { name: string; orgs: number[] };
const schema = createExcelSchema<Row>()
.column("name", { accessor: "name" })
.group("memberships", (b, orgIds: number[]) => {
for (const id of orgIds) {
b.column(`org-${id}`, { accessor: (r) => r.orgs.includes(id) });
}
})
.build();
createWorkbook()
.sheet("Sheet")
.table({
rows: [],
schema,
select: { exclude: ["memberships"] },
});
Because the only contextful group is excluded here, context is not needed at all.
Only groups that declare a context parameter contribute to SchemaGroupContext, and only selected contextful groups make context required.