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
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps
typescript
import { useEffect, useState } from "react";
 
interface Section {
  id: string;

Building a scroll-spy hook in React

custom-hooks intersectionobserver dom-observation
Intermediate 8 steps
typescript
import { Injectable, Scope, Inject, NotFoundException } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
import { DataSource } from 'typeorm';

Per-tenant database connections in NestJS

multi-tenancy connection-pooling dependency-injection
Advanced 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