typescript 57 lines · 8 steps

A type-safe SQL query builder in TypeScript

A generic builder chains type-checked clauses and emits parameterized SQL to prevent injection.

Explained by highlit
1type Operator = "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
2 
3type Row = Record<string, unknown>;
4 
5type Condition<T> = {
6 column: keyof T & string;
7 operator: Operator;
8 value: unknown;
9};
10 
11class QueryBuilder<T extends Row> {
12 private conditions: Condition<T>[] = [];
13 private columns: (keyof T & string)[] = [];
14 private orderBy?: { column: keyof T & string; dir: "ASC" | "DESC" };
15 private limitCount?: number;
16 
17 constructor(private readonly table: string) {}
18 
19 select(...columns: (keyof T & string)[]): this {
20 this.columns = columns;
21 return this;
22 }
23 
24 where(column: keyof T & string, operator: Operator, value: unknown): this {
25 this.conditions.push({ column, operator, value });
26 return this;
27 }
28 
29 order(column: keyof T & string, dir: "ASC" | "DESC" = "ASC"): this {
30 this.orderBy = { column, dir };
31 return this;
32 }
33 
34 limit(count: number): this {
35 this.limitCount = count;
36 return this;
37 }
38 
39 build(): { sql: string; params: unknown[] } {
40 const cols = this.columns.length ? this.columns.join(", ") : "*";
41 const params: unknown[] = [];
42 let sql = `SELECT ${cols} FROM ${this.table}`;
43 
44 if (this.conditions.length) {
45 const clauses = this.conditions.map((c) => {
46 params.push(c.value);
47 return `${c.column} ${c.operator} $${params.length}`;
48 });
49 sql += ` WHERE ${clauses.join(" AND ")}`;
50 }
51 
52 if (this.orderBy) sql += ` ORDER BY ${this.orderBy.column} ${this.orderBy.dir}`;
53 if (this.limitCount != null) sql += ` LIMIT ${this.limitCount}`;
54 
55 return { sql, params };
56 }
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Constraining a generic to a row shape lets column names be validated at compile time.
  2. 2Returning `this` from each method enables fluent chaining until a terminal build step.
  3. 3Pushing values into a params array and emitting placeholders keeps SQL parameterized and injection-safe.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A type-safe SQL query builder in TypeScript — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code