Last Updated: 3/20/2026
Defining Database Types
Kysely’s type safety depends on a TypeScript interface that describes your database schema. This Database interface maps table names to table schema interfaces, and Kysely uses it to validate every query you write.
The Database Interface
The Database interface is a simple object type where keys are table names and values are table schema interfaces:
interface Database {
person: PersonTable
pet: PetTable
}Each table interface describes the columns in that table. Important: Table interfaces should only be used in the Database type. Never use them as the result type of a query. Instead, use the Selectable, Insertable, and Updateable utility types (explained below).
Defining Table Interfaces
A table interface lists every column with its TypeScript type:
interface PersonTable {
id: Generated<number>
first_name: string
last_name: string | null
age: number
created_at: ColumnType<Date, string | undefined, never>
}Column Types
Simple columns use plain TypeScript types:
first_name: string
age: numberNullable columns use union types with null:
last_name: string | nullDo not use optional properties (last_name?: string). Kysely determines optionality automatically based on the operation (select, insert, update). A nullable column is always present in select results (as string | null), but it’s optional in inserts and updates.
Generated Columns
The Generated<T> type marks columns that are auto-generated by the database (like auto-incrementing primary keys):
id: Generated<number>This makes the column optional in inserts and updates, but always present (and non-optional) in selects. The Generated type is defined in src/util/column-type.ts as:
type Generated<S> = ColumnType<S, S | undefined, S>It’s a shorthand for ColumnType<number, number | undefined, number>, meaning:
- Select type:
number(always present) - Insert type:
number | undefined(optional) - Update type:
number(optional, because all update columns are optional)
ColumnType for Different Operations
The ColumnType<SelectType, InsertType, UpdateType> utility lets you specify different types for select, insert, and update operations:
created_at: ColumnType<Date, string | undefined, never>This means:
- Selects return a
Dateobject (the driver parses the database timestamp) - Inserts accept a
stringorundefined(you can provide an ISO string or let the database default) - Updates use
never(the column cannot be updated)
The ColumnType type is defined in src/util/column-type.ts. It’s a branded type with three phantom properties:
type ColumnType<SelectType, InsertType = SelectType, UpdateType = SelectType> = {
readonly __select__: SelectType
readonly __insert__: InsertType
readonly __update__: UpdateType
}These properties never exist at runtime — they’re purely for TypeScript’s type system.
GeneratedAlways Columns
For columns that are always generated by the database (like PostgreSQL’s GENERATED ALWAYS AS IDENTITY), use GeneratedAlways<T>:
id: GeneratedAlways<number>This is equivalent to ColumnType<number, never, never> — the column is read-only and cannot be inserted or updated.
JSON Columns
The JSONColumnType<T> utility is a shorthand for JSON columns:
metadata: JSONColumnType<{
login_at: string
ip: string | null
plan: 'free' | 'premium'
}>This expands to ColumnType<T, string, string>, meaning:
- Selects return the parsed JSON object (type
T) - Inserts and updates accept a JSON string
The driver handles JSON parsing/stringification. Kysely just provides the types.
Extracting Row Types
Table interfaces describe the database schema, but you shouldn’t use them directly in your application code. Instead, use these utility types from src/util/column-type.ts:
Selectable<T>
Extracts the select type from a table interface:
type Person = Selectable<PersonTable>
// {
// id: number
// first_name: string
// last_name: string | null
// age: number
// created_at: Date
// }Use this type for query results.
Insertable<T>
Extracts the insert type from a table interface:
type NewPerson = Insertable<PersonTable>
// {
// id?: number
// first_name: string
// last_name?: string | null
// age: number
// created_at?: string
// }Use this type for insert payloads. Notice that id and created_at are optional (because they’re Generated or have undefined in their insert type), and last_name is optional because it’s nullable.
Updateable<T>
Extracts the update type from a table interface:
type PersonUpdate = Updateable<PersonTable>
// {
// id?: number
// first_name?: string
// last_name?: string | null
// age?: number
// // created_at is missing because its update type is `never`
// }All columns are optional in updates (you only update the fields you want to change). Columns with never as their update type are excluded entirely.
Complete Example
Here’s a complete database schema with two tables:
import { ColumnType, Generated, Insertable, Selectable, Updateable } from 'kysely'
export interface Database {
person: PersonTable
pet: PetTable
}
export interface PersonTable {
id: Generated<number>
first_name: string
last_name: string | null
age: number
created_at: ColumnType<Date, string | undefined, never>
}
export interface PetTable {
id: Generated<number>
name: string
owner_id: number
species: 'dog' | 'cat'
}
// Row types for application code
export type Person = Selectable<PersonTable>
export type NewPerson = Insertable<PersonTable>
export type PersonUpdate = Updateable<PersonTable>
export type Pet = Selectable<PetTable>
export type NewPet = Insertable<PetTable>
export type PetUpdate = Updateable<PetTable>Keeping Types in Sync
Your TypeScript types must match your actual database schema. If they drift, you’ll get runtime errors even though TypeScript says everything is fine.
For production applications, use a code generator to automatically create your Database interface by introspecting your database or Prisma schema. Popular options include:
- kysely-codegen — Introspects your database and generates types
- prisma-kysely — Generates Kysely types from Prisma schemas
These tools ensure your types always match your schema. Manual type definitions work for small projects but become error-prone as schemas grow.
Runtime vs. Compile-Time Types
Kysely only deals with compile-time types. The runtime JavaScript types (what you actually get from the database) are determined by the underlying driver (pg, mysql2, etc.).
For example, if your database stores a TIMESTAMP column, the driver might return a Date object, a string, or a number depending on its configuration. Kysely doesn’t transform these values — it just provides TypeScript types that match what the driver returns.
If the driver returns a Date but your table interface says string, you’ll get a type error when you try to use the result. Always verify what your driver returns and adjust your table interface accordingly.
What’s Next
- First Query: Write your first SELECT query with Kysely and see how TypeScript validates every column reference in real time.