Last Updated: 3/20/2026
Dynamic Module
Kysely’s type system validates table and column names at compile time. This is powerful, but sometimes you need to reference tables or columns whose names are determined at runtime—from user input, configuration files, or dynamic query builders. The db.dynamic module provides escape hatches for these scenarios.
Dynamic Column References
Use db.dynamic.ref() to create a reference to a column whose name isn’t known at compile time:
async function filterByColumn(
columnName: string,
value: string
) {
const { ref } = db.dynamic
return await db
.selectFrom('person')
.selectAll()
.where(ref(columnName), '=', value)
.execute()
}
await filterByColumn('first_name', 'Jennifer')
await filterByColumn('last_name', 'Aniston')The ref() method (defined in src/dynamic/dynamic.ts) returns a DynamicReferenceBuilder that bypasses TypeScript’s type checking. Kysely treats the reference as valid and includes it in the generated SQL.
Warning: Column names are not escaped or validated. If you pass unchecked user input to ref(), you create an SQL injection vulnerability. Always validate column names against a whitelist:
const ALLOWED_COLUMNS = ['first_name', 'last_name', 'age'] as const
async function filterByColumn(
columnName: string,
value: string
) {
if (!ALLOWED_COLUMNS.includes(columnName as any)) {
throw new Error(`Invalid column: ${columnName}`)
}
const { ref } = db.dynamic
return await db
.selectFrom('person')
.selectAll()
.where(ref(columnName), '=', value)
.execute()
}Type-Safe Dynamic References
Provide a type parameter to ref() to constrain the allowed column names:
type PersonColumn = 'first_name' | 'last_name' | 'age'
async function filterByColumn(
columnName: PersonColumn,
value: string
) {
const { ref } = db.dynamic
return await db
.selectFrom('person')
.selectAll()
.where(ref<PersonColumn>(columnName), '=', value)
.execute()
}
// TypeScript error: Argument of type '"invalid"' is not assignable to parameter of type 'PersonColumn'
await filterByColumn('invalid', 'value')The type parameter doesn’t validate the runtime value, but it restricts what values TypeScript allows you to pass. This provides some safety while still allowing runtime flexibility.
Dynamic Selections
Use dynamic references in select clauses to choose columns at runtime:
type PersonColumn = 'first_name' | 'last_name' | 'age'
async function selectColumns(columns: PersonColumn[]) {
const { ref } = db.dynamic
return await db
.selectFrom('person')
.select(columns.map(col => ref<PersonColumn>(col)))
.execute()
}
const result = await selectColumns(['first_name', 'age'])
// result type: Array<{ first_name?: string, last_name?: string, age?: number }>When you use dynamic references in selections, the result type includes all possible columns as optional properties. TypeScript can’t know which columns were actually selected until runtime, so it assumes any of them might be present.
Dynamic Order By
Dynamic references work in orderBy clauses:
type PersonColumn = 'first_name' | 'last_name' | 'age'
async function sortBy(column: PersonColumn, direction: 'asc' | 'desc') {
const { ref } = db.dynamic
return await db
.selectFrom('person')
.selectAll()
.orderBy(ref<PersonColumn>(column), direction)
.execute()
}
await sortBy('age', 'desc')Dynamic Table References
Use db.dynamic.table() to reference a table whose name is determined at runtime:
type TableName = 'person' | 'pet'
async function findById<T extends TableName>(
tableName: T,
id: number
) {
const { table } = db.dynamic
return await db
.selectFrom(table(tableName).as('t'))
.selectAll()
.where('t.id', '=', id)
.executeTakeFirst()
}
const person = await findById('person', 1)
const pet = await findById('pet', 2)The table() method returns a DynamicTableBuilder (defined in src/dynamic/dynamic-table-builder.ts) that must be aliased with .as() before use. The alias provides a stable reference point for the table in the query.
Generic Query Helpers
Combine dynamic table and column references to create generic query helpers:
import { SelectType } from 'kysely'
async function findByColumn<
T extends keyof Database,
C extends keyof Database[T] & string,
V extends SelectType<Database[T][C]>
>(
tableName: T,
columnName: C,
value: V
) {
const { table, ref } = db.dynamic
return await db
.selectFrom(table(tableName).as('t'))
.selectAll()
.where(ref(columnName), '=', value)
.execute()
}
const people = await findByColumn('person', 'first_name', 'Jennifer')
const pets = await findByColumn('pet', 'species', 'dog')This helper is fully type-safe: TypeScript validates that columnName is a valid column of tableName, and that value has the correct type for that column.
When to Use Dynamic References
Dynamic references are an escape hatch. Use them only when necessary:
Good use cases:
- User-configurable sort columns in a UI
- Dynamic filtering based on query parameters
- Generic utility functions that work across multiple tables
- Building queries from configuration files
Bad use cases:
- Queries where the table and column names are known at compile time
- Avoiding TypeScript errors by bypassing type checking
- Working around schema design issues
If you find yourself using dynamic references frequently, consider whether your schema or query design could be improved.
The DynamicReferenceBuilder Class
The DynamicReferenceBuilder class (defined in src/dynamic/dynamic-reference-builder.ts) implements the OperationNodeSource interface. It stores the column reference as a string and converts it to a SimpleReferenceExpressionNode when the query is compiled.
The class includes a type parameter R that represents the column name type. This parameter is used only for type checking and has no runtime effect:
class DynamicReferenceBuilder<R extends string = never> {
constructor(reference: string)
toOperationNode(): SimpleReferenceExpressionNode
}The DynamicTableBuilder Class
The DynamicTableBuilder class (defined in src/dynamic/dynamic-table-builder.ts) stores a table name and provides an .as() method to create an aliased table reference:
class DynamicTableBuilder<T extends string> {
constructor(table: T)
as<A extends string>(alias: A): AliasedDynamicTableBuilder<T, A>
}The AliasedDynamicTableBuilder implements OperationNodeSource and converts to an AliasNode containing the table reference and alias.
Combining Dynamic and Static References
You can mix dynamic and static references in the same query:
type SortColumn = 'first_name' | 'last_name' | 'age'
async function getPeople(sortBy: SortColumn) {
const { ref } = db.dynamic
return await db
.selectFrom('person') // Static table reference
.select(['id', 'first_name', 'last_name']) // Static column references
.orderBy(ref<SortColumn>(sortBy), 'asc') // Dynamic order by
.execute()
}This gives you type safety where possible while allowing runtime flexibility where needed.
Security Considerations
Dynamic references bypass Kysely’s type checking and parameter binding for identifiers. This creates two security risks:
SQL Injection: Column and table names are inserted directly into the SQL string without escaping. Always validate dynamic names against a whitelist.
Type Safety Bypass: TypeScript can’t verify that a dynamic reference is valid. Runtime errors will occur if you reference a non-existent column or table.
To mitigate these risks:
- Validate all dynamic names against a whitelist of allowed values
- Use type parameters to constrain the allowed values at compile time
- Test thoroughly to catch runtime errors
- Prefer static references whenever possible
What’s Next
- Extending Kysely: Create custom expressions and helpers using the
Expressioninterface. - Raw SQL: Use the
sqltemplate tag for complex expressions that don’t fit Kysely’s type system.