Write raw SQL in TypeScript with full type safety — no type casts required!

This repository is a proof of concept (POC) for a typed query function in TypeScript. It allows you to write raw SQL queries and automatically infer the correct return types for SELECT
statements. This provides a lightweight alternative to an ORM, keeping your database as the single source of truth for your schema.
- Define your database schema and seed it with data.
- Write raw SQL queries in
.sql
files. - Run
pnpm db:gen
to generate TypeScript types for your queries. - Use the
query
function in your application code to execute the queries and access typed results.
- SQLite (via
@libsql/client
)
Support for additional databases may be added in the future.
Create a SQLite database with the required tables and data. For example, in this POC, the database includes tables for users
, roles
, and countries
.
Adjust the database configuration in the file typed_sql/script.ts
.
For each query you want to use in your application, create a .sql
file in a queries
folder. For example:
-- queries/users.sql
SELECT
u.id,
u.username,
u.country_code AS location,
r.rolename AS role
FROM
users u
INNER JOIN roles r ON u.role_id = r.id
ORDER BY
u.created_at
Run the following command to generate TypeScript types for your queries:
pnpm db:gen
This script:
- Connects to your database to retrieve the table schemas.
- Parses the SQL queries to infer the correct return types.
Import the query
function and pass the filename of the SQL file to execute the query:
import { query } from './typed_sql/db'
const { rows, err } = await query('users')
The rows
object will have the following inferred type:
const rows:
| {
id: number
username: string
location: string | null
role: string
}[]
| null
The rows
array contains the results of the query, with each row typed according to the SQL query's output.
If an error occurs during the query, rows
will be null
, and the err
object of type LibsqlError
will contain the error details. This is already logged in the query
function, so usually you only need to process it further if you want to know the error code, for example, or forward the error message elsewhere.
-
Support queries with parameters.
-
Support
INSERT
andUPDATE
queries that end withRETURNING ...
. -
Support subqueries.
-
Make the approach agnostic to
@libsql/client
.