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

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.

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