Dynamic Columns
.dynamic(id, build) creates runtime-generated columns from table-time context.
Use it when the child columns depend on runtime inputs such as regions, months, organizations, or categories.
Dynamic scopes also support condition: ({ ctx }) => boolean in the optional options object when the entire runtime-generated scope should be included or omitted from schema context.
Runtime-generated columns with .dynamic()
dynamic() receives the builder plus { ctx }, where ctx is the schema-level context object declared on createExcelSchema<Row, Context>().
import { createExcelSchema } from "typed-xlsx";
type User = {
firstName: string;
organizations: Array<{ id: number; name: string }>;
};
type SchemaContext = {
orgs: Array<{ id: number; name: string }>;
};
const schema = createExcelSchema<User, SchemaContext>()
.column("firstName", { accessor: "firstName", header: "Name" })
.dynamic("orgs", (builder, { ctx }) => {
for (const org of ctx.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
Dynamic scopes receive their data when the table is built, not when the schema is defined.
import { createExcelSchema, createWorkbook } from "typed-xlsx";
type User = {
firstName: string;
organizations: Array<{ id: number; name: string }>;
};
type SchemaContext = {
orgs: Array<{ id: number; name: string }>;
};
const schema = createExcelSchema<User, SchemaContext>()
.column("firstName", { accessor: "firstName", header: "Name" })
.dynamic("orgs", (builder, { ctx }) => {
for (const org of ctx.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("users", {
rows: users,
schema,
context: { orgs },
});
The schema definition stays static. The dynamic child columns are generated fresh from the context passed into .table().
For the full schema-context model, including where else ctx is available and why context stays required independently from select, see Schema Context.
Use with column selection
Selection works at the dynamic-scope level. Use the declared dynamic ID, not the generated child column IDs.
import { createExcelSchema, createWorkbook } from "typed-xlsx";
type Row = { name: string; orgs: number[] };
const schema = createExcelSchema<Row, { orgIds: number[] }>()
.column("name", { accessor: "name" })
.dynamic("memberships", (builder, { ctx }) => {
for (const id of ctx.orgIds) {
builder.column(`org-${id}`, { accessor: (row) => row.orgs.includes(id) });
}
})
.build();
createWorkbook()
.sheet("Sheet")
.table("sheet", {
rows: [],
schema,
context: { orgIds: [] },
select: { exclude: ["memberships"] },
});
Even though the only dynamic scope is excluded here, context is still required because it is part of the schema contract.
Dynamic scopes and formulas
Formula columns inside a dynamic scope follow the same predecessor-based rules as top-level formulas.
A formula declared inside .dynamic() can reference:
- any column declared before the scope in the outer chain
- any column declared before it within the same dynamic scope
Later formulas can aggregate the dynamic scope with refs.dynamic(...) and helpers like fx.sum(...) or fx.average(...).
For the full scope model, see Scope & References.