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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Constraining a generic to a row shape lets column names be validated at compile time.
- 2Returning `this` from each method enables fluent chaining until a terminal build step.
- 3Pushing values into a params array and emitting placeholders keeps SQL parameterized and injection-safe.
Related explainers
typescript
export function isValidCardNumber(input: string): boolean { const digits = input.replace(/[\s-]/g, ""); if (!/^\d{12,19}$/.test(digits)) {
Validating card numbers with the Luhn check
luhn-algorithm
checksum
input-validation
Intermediate
7 steps
typescript
interface UserAgentInfo { browser: { name: string; version: string }; os: { name: string; version: string }; device: 'mobile' | 'tablet' | 'desktop';
Parsing a user-agent string with ordered rules
regex
parsing
pattern-matching
Intermediate
9 steps
typescript
import { Injectable, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, timer, throwError } from 'rxjs'; import { switchMap, takeWhile, filter, take, catchError } from 'rxjs/operators';
Polling a job until it finishes in Angular
rxjs
polling
observables
Intermediate
7 steps
typescript
type Flatten = Record<string, unknown>; function isPlainObject(value: unknown): value is Record<string, unknown> { return (
Flattening nested objects into dotted keys
recursion
reduce
type-guards
Intermediate
7 steps
typescript
import { Injectable, signal, computed, effect, inject } from '@angular/core'; import { DOCUMENT } from '@angular/common'; export type Theme = 'light' | 'dark';
A signal-based theme service in Angular
signals
reactivity
dependency-injection
Intermediate
7 steps
typescript
import { Injectable } from '@angular/core'; import { HttpClient, HttpEventType, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, distinctUntilChanged, scan } from 'rxjs/operators';
Tracking upload progress in Angular
rxjs
http-events
state-reduction
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-type-safe-sql-query-builder-in-typescript-explained-typescript-8220/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.