
Security News
PolinRider: North Korea-Linked Supply Chain Campaign Expands Across Open Source Ecosystems
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.
@kuindji/typed-sql
Advanced tools
A compile-time SQL validator and result-type inferrer for TypeScript.
You write SQL as a normal TypeScript string. The library parses and checks it
entirely in the type system — against a schema you describe as a type — and
infers the shape of the rows the query returns. Nothing runs at runtime for the
validation/inference: the work happens while tsc type-checks your code.
import type { ValidateSQL, GetReturnType, DatabaseSchema } from "@kuindji/typed-sql";
type Schema = {
defaultSchema: "public";
schemas: {
public: {
users: { id: number; email: string; name: string | null };
};
};
};
type Ok = ValidateSQL<"select id, email from users", Schema>; // true
type Bad = ValidateSQL<"select id, nope from users", Schema>; // false
type Rows = GetReturnType<"select id, name from users", Schema>; // { id: number; name: string | null }
Target dialect: PostgreSQL. Quoted identifiers (
"camelCase"),::casts,coalesce,distinct on,returning, etc. are interpreted with Postgres semantics.
DatabaseSchema type, it confirms that
tables, columns, aliases, and references in a query actually exist, and rejects
ones that don't.GetReturnType<Q, Schema> produces the row object a
SELECT/RETURNING query yields, including join nullability and casts.createSelectQuery, createConditionTree,
conditional-SQL helpers) that assembles a SQL string + ordered params and
carries the inferred result type alongside it.createSelectFn(driver)
takes your executor and just hands it the assembled (sql, params) — you
bring the database client.unknown (see below).type DatabaseSchema = {
defaultSchema: string;
schemas: Record<string /* schema */, Record<string /* table */, Record<string /* column */, /* TS type */ unknown>>>;
};
T | null (e.g. name: string | null)."a" | "b" enums, arrays, nested
JSON-shaped objects, Record<string, unknown>.type Valid = ValidateSQL<"update users set name = $1 where id = $2", Schema>; // true | false
type Row = GetReturnType<"select id, name from users where id = $1", Schema>;
// DML helpers
type InsertCols = GetInsertTableColumns<"insert into users ...", Schema>;
type UpdateCols = GetUpdateTableColumns<"update users set ...", Schema>;
import { createSelectQuery, createSelectFn } from "@kuindji/typed-sql";
const q = createSelectQuery<Schema>()
.from("users u")
.select("u.id")
.where("u.id = :id")
.withParams({ id: 42 });
q.toString(); // "SELECT u.id FROM users u WHERE u.id = $1"
[...q.getParams()]; // [42] ← named params expanded to $1, $2… in order
// DISTINCT / DISTINCT ON (PostgreSQL). Neither changes the inferred row shape.
createSelectQuery<Schema>().from("users u").select("u.id").distinct().toString();
// "SELECT DISTINCT u.id FROM users u"
createSelectQuery<Schema>()
.from("users u").select("u.id").distinctOn("u.email").orderBy("u.email").toString();
// "SELECT DISTINCT ON (u.email) u.id FROM users u ORDER BY u.email"
// Wire YOUR driver. The library never touches the DB itself.
const select = createSelectFn<Schema>((sql, params) => pg.query(sql, params));
const rows = await select(q); // rows typed from the builder's inferred result
import { createInsertQuery, createMutateFn, createSql } from "@kuindji/typed-sql";
const q = createInsertQuery<Schema>()
.into("orders")
.value("userId", ":uid") // :uid typed to orders.userId's exact (branded) type
.value("amount", ":amt")
.valueIf(hasNote, "note", ":note") // conditional → :note optional in withParams
.returning("id")
.withParams({ uid, amt, ...(hasNote ? { note } : {}) });
q.toString(); // "insert into orders (userId, amount) values ($1, $2) returning id"
[...q.getParams()]; // [uid, amt]
// Raw typed SQL:
const sql = createSql<Schema>();
const d = sql("delete from orders where id = :id").withParams({ id });
// Executor — bring your driver; it returns the RETURNING rows (or [] when none):
const mutate = createMutateFn<Schema>((s, p) => pool.query(s, p).then(r => r.rows));
const rows = await mutate(q); // typed from RETURNING
// Passing a plain string where a branded column is expected is a compile error.
// Multi-row VALUES is rejected in the typed path — use the untyped driver call.
A few deliberate behaviors you'll observe when using the library:
unknown. The inferrer types an expression
only when its type is unambiguous — CASE and unmodeled functions are
unknown rather than a guess. || (string concat) → string. An unaliased
function/aggregate projection is named after the function (count(*) →
{ count: number }); an unaliased CASE is named case.select 'GBP' as cur →
{ cur: string }, select 42 as n → { n: number }, not { cur: "GBP" } /
{ n: 42 }. Locked literal types reject every other value in mutable
bindings, useState, props, etc.; add an explicit cast at the call site when
you want the literal back.true. Very
large/complex queries may fall back to unknown/true rather than failing
(TypeScript's recursion limits put a hard ceiling on type-level parsing).| null to columns sourced from the
nullable side (left join … x ⇒ x.col becomes T | null). This applies
inside coalesce(...) too: the result is nullable only if every argument
is (Postgres semantics), so coalesce(x, '') stays non-null.*If) — runtime vs type-levelThe builder's *If methods — selectIf, whereIf, joinIf, groupByIf,
havingIf, orderByIf, limitIf, offsetIf, and applyIf — take a runtime
boolean as their first argument. This creates a deliberate gap between what runs
and what the types say:
selectIf(false, "name") adds nothing to the query.*If fragment treated as present — and then marks columns that might be
absent as optional.Per method:
selectIf / applyIf that introduce a column → that column becomes an
optional property in the result row (name?: T, i.e. T | undefined at the
use site). Unconditional select/apply columns stay required, regardless
of call order.select at all, the all-false runtime path
emits SELECT *, so the whole row falls back to Partial<…> — every column
optional.*If (whereIf, joinIf, groupByIf, havingIf, orderByIf,
limitIf, offsetIf) conditionally changes the SQL text at runtime but does
not change the result column set — the type is computed as if the clause is
present.const dyn: boolean = /* computed at runtime */;
const q = createSelectQuery<Schema>()
.from("users")
.select("id") // unconditional → required
.selectIf(dyn, "name"); // conditional → optional
type Row = BuilderReturnType<typeof q>;
// { id: number; name?: string } ← id required; name is `string | undefined`
| null vs optional (| undefined)These look similar but mean different things:
| Source | Type shape | Meaning | |
|---|---|---|---|
| null | LEFT/outer join (nullable side) | col: T | null — key always present | The column is in every row, but its value can be SQL NULL (the join didn't match). |
optional (| undefined) | selectIf / applyIf conditional projection | col?: T — key may be absent | The column may not be in the result object at all, because it wasn't selected at runtime. |
A left-joined column that is also conditionally selected is both:
col?: T | null.
Contributing or reviewing? See CONTRIBUTING.md for the design contracts, internals, and things that look like bugs but are intended.
MIT © Ivan Kuindzhi
FAQs
Type-level SQL parser for TypeScript
We found that @kuindji/typed-sql demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.

Security News
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.