Skip to Content
🔧 AdvancedDynamic Module

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:

  1. Validate all dynamic names against a whitelist of allowed values
  2. Use type parameters to constrain the allowed values at compile time
  3. Test thoroughly to catch runtime errors
  4. Prefer static references whenever possible

What’s Next

  • Extending Kysely: Create custom expressions and helpers using the Expression interface.
  • Raw SQL: Use the sql template tag for complex expressions that don’t fit Kysely’s type system.