Skip to content

Getting started ​

typesql infers types from SQL you write as string literals. You describe your database once as a TypeScript interface; every sql("…") call is then parsed by the compiler, which works out the row type, the parameter types and any mistakes.

Requirements ​

  • TypeScript 7 (the native compiler). The engine is tuned for its speed and limits.
  • Node.js 26 or later.
  • PostgreSQL. typesql models Postgres semantics only.

Install ​

Install the core package and the adapter for your driver:

sh
pnpm add @typesql/core @typesql/pg pg              # node-postgres
pnpm add @typesql/core @typesql/postgres postgres  # or postgres.js

@typesql/core has no runtime dependencies. The adapters add a type map that matches the driver's parsers and a few helpers to run queries.

If you use Kysely, Drizzle or Prisma, their packages (@typesql/kysely, @typesql/drizzle, @typesql/prisma) can turn the schema you already have into the one typesql needs. See ORM schemas.

Describe the schema ​

Columns are declared with Postgres type names, not TypeScript types. The type map turns them into TypeScript types at the end, so the same schema works with any driver.

ts
export interface DB {
  users: {
    id: Generated<"int4">; // serial / identity / DEFAULT: optional on INSERT
    email: Col<"text">; // NOT NULL
    display_name: Nullable<"text">; // NULL allowed
    mood: Enum<"mood", { hasDefault: true }>; // a CREATE TYPE … AS ENUM with a DEFAULT
    created_at: Generated<"timestamptz">;
  };
  posts: {
    id: Generated<"int8">;
    author_id: Col<"int4">;
    title: Col<"text">;
    body: Nullable<"text">;
    tags: Generated<"text[]">;
  };
  user_stats: View<{ user_id: Col<"int4">; posts: Col<"int8"> }>;
  $types: { mood: "happy" | "neutral" | "sad" };
}

Col is NOT NULL, Nullable allows NULL, and Generated marks a column with a default, which makes it optional in INSERT. See Schema and type maps for the full list, Migrations to derive this interface from your DDL instead, and ORM schemas to take it from Kysely, Drizzle or Prisma.

Write a query ​

ts
import { Pool } from "pg";
import { createSql, query } from "@typesql/pg";
import type { DB } from "./schema.js";

const sql = createSql<DB>();
const pool = new Pool({ connectionString: process.env["DATABASE_URL"] });

const recent = sql(
  `SELECT u.id, u.email, p.title
   FROM users u
   LEFT JOIN posts p ON p.author_id = u.id
   WHERE u.created_at > $1`,
  new Date("2026-01-01"),
);

const rows = await query(pool, recent);
// rows: { id: number; email: string; title: string | null }[]

The LEFT JOIN makes title nullable, and $1 takes the type of created_at. The query object is plain data; the text is sent to Postgres unchanged:

ts
recent.text; // "SELECT u.id, u.email, p.title FROM users u …"  (sent as is)
recent.values; // [Date]

Mistakes are compile errors ​

When the SQL is wrong, the error appears on the string, worded like the Postgres error you would get at runtime:

ts
sql("SELECT emial FROM users");
// Argument of type '"SELECT emial FROM users"' is not assignable to parameter of type
//   'SqlError<"column \"emial\" does not exist. Perhaps you meant the column
//    \"email\"?", 1> & "SELECT emial FROM users"'.

sql("SELECT id FROM users WHERE id = $1", "7");
// Argument of type 'string' is not assignable to parameter of type 'number'.

The number after the message is the index of the offending token.

Next steps ​

  • Queries: what gets inferred, and how nullability works.
  • Parameters: $1, :name and inline ${sql.param(value)}.
  • Fragments: building statements from pieces.
  • Writing data: INSERT, UPDATE, DELETE and RETURNING.
  • ORM schemas: reuse a Kysely, Drizzle or Prisma schema.