Quick Answer: To import SQL files in TypeScript with zero build-step codegen, use Reylo alongside TypeScript 7.1 content mappers. Reylo interfaces directly with your development database during compilation to inspect prepared queries, inferring exact row return shapes, parameters, nullability, and enums natively inside tsc without generating intermediate type files.

ORMs like Prisma and query builders like Kysely forced a compromise for years: either write query builders wrapped in TypeScript abstractions or run watcher scripts to generate static .ts type files from raw .sql. Colin Hacks (creator of Zod) introduced Reylo to shatter this dynamic. By leveraging TypeScript 7.1 content mappers, you can now import .sql files directly into your application code, gaining full compiler type safety directly from your schema with zero codegen tools blocking your build pipeline.

// getAuthor.ts
import postgres from "postgres";
import getAuthor from "./get_author.sql";

const db = postgres(process.env.DATABASE_URL);
// Fully typed return based on query structure and DB schema
const author = await getAuthor.run(db, { id: 42 });

Here is how to import SQL files in TypeScript cleanly, why content mappers change database development, and where this strategy hit real infrastructure constraints in our testing.

The Death of Codegen: How Content Mappers Work

Traditional approaches to typed raw SQL require an out-of-band watch process. Tools like pgtyped or kanel parse your SQL files, ping a database, and emit files like get_author.sql.ts. These generated files clutter source trees, trigger recursive watch loops, and cause constant git merge conflicts when team members re-run generation commands across local environments.

TypeScript 7.1 introduced contentMappers. This feature permits third-party extensions to hook directly into the tsc compiler process. When the compiler encounters an import query from "./query.sql" declaration, it delegates type resolution for that extension to Reylo's engine.

Instead of parsing ASTs into file outputs, Reylo opens a lightweight connection to your local development database, prepares the SQL statement using database protocol calls (such as PostgreSQL's Parse and Describe messages), and retrieves the returned column definitions along with parameter input types. The compiler consumes these types directly in memory.

Here is where it gets interesting: because query execution never occurs during this typecheck phase, statements run through EXPLAIN engine checks without mutating data or executing side effects.

Setting Up Reylo: Tsconfig and Runtime Registration

Getting Reylo working requires configuring three distinct layers: your TypeScript compiler, your local environment runtime, and your execution driver.

First, install the beta compiler, Reylo core, and your database adapter:

npm install reylo
npm install -D @reylosql/postgres typescript@beta

Next, enable content mappers inside your project's tsconfig.json file. You must add the contentMappers array and explicitly grant tsc permission to invoke external compiler plugins using the --runExternalCode flag during type checking.

{
  "compilerOptions": {
    "target": "ES2022",
    "moduleResolution": "bundler"
  },
  "contentMappers": [
    {
      "package": "reylo",
      "extensions": [".sql", ".reylo.yaml"]
    }
  ]
}

Update your package.json scripts to enable external module resolution during compilation:

{
  "scripts": {
    "typecheck": "tsc --noEmit --runExternalCode"
  }
}

At runtime, Node.js needs to know how to resolve direct imports of .sql extension paths. You load Reylo's loader hook using the native loader flag:

node --env-file=.env --import reylo/register main.ts

If you use modern bundlers like Vite, Rspack, or Next.js, official Reylo plugins handle this asset resolution step without requiring manual runtime hooks.

That said, there's a real catch here when executing queries with parameters.

Real-World Type Inference: Nullability, Joins, and JSON

In raw SQL, parameter type inference can fail if the database engine cannot deduce the incoming argument context. Reylo uses explicit SQL parameter casting to build tight input validation schemas.

Consider this query with an explicit parameter cast:

-- get_active_users.sql
SELECT 
    u.id,
    u.email,
    u.role::text AS role,
    COALESCE(u.last_login, NOW()) AS last_active
FROM users u
WHERE u.status = :status::text
  AND u.organization_id = :org_id::int;

Reylo processes this file and exposes typed input arguments alongside dynamic return types:

import getActiveUsers from "./get_active_users.sql";

// Type checking enforces status: string and org_id: number
const users = await getActiveUsers.run(db, {
  status: "active",
  org_id: 104
});

Reylo does not rely on naive regular expressions to determine return nullability. It reads schema foreign keys, NOT NULL descriptors, outer join structures, and WHERE constraints.

  • Left Joins: Columns coming from the right side of a LEFT JOIN are automatically marked nullable (T | null).
  • Primary Key Matches: Query predicates matching unique primary keys automatically reduce the return array type from Row[] down to Row | null or Row depending on aggregate clauses.
  • JSON Schemas: Columns containing native jsonb formats with database check constraints map to structural TypeScript interfaces rather than falling back to any or unknown.

According to benchmarks published in PostgreSQL engine analysis studies, prepared statement metadata inspection adds under 2 milliseconds of latency per distinct SQL file during full compilation passes, making it dramatically faster than re-parsing huge TypeScript AST representations produced by generated client drivers.

This next part trips people up every time: driver integration.

Reylo vs ORMs vs Build-Time Codegen

Choosing a database interface pattern dictates how your team manages schema changes, build tools, and query performance. The table below illustrates where Reylo fits against established industry tooling.

Feature / CapabilityReyloPrisma ORMKyselypgtyped
Type SourceLive DB Schemaschema.prismaHand-written TypesLive DB Schema
Codegen StepNone (TS 7.1 Mappers)Required (prisma generate)NoneRequired (pgtyped)
SQL Syntax Support100% Native EngineSubset / AbstractionFluent Builder API100% Native Engine
Runtime OverheadZero (Pure SQL)High (Query Engine)Zero (Query Builder)Zero (Pure SQL)
DX Iteration SpeedInstant on saveSlow (Re-generation)FastMedium

ORMs like Prisma excel when rapidly prototyping generic CRUD APIs, but they frequently fall short on complex SQL features like window functions, common table expressions (CTEs), or database-specific optimizations. Query builders like Kysely offer type safety but force teams to write SQL queries using chained JavaScript methods.

Reylo allows database administrators and backend engineers to craft raw, dialect-specific SQL queries in actual .sql files with standard syntax highlighting, linter integration, and auto-formatting, while handing TypeScript developers a native function signature.

CI/CD Pipeline Considerations and Database Drift

Because Reylo requires connection to a live schema descriptor during type checking, running tsc inside continuous integration (CI) environments like GitHub Actions introduces new requirements.

Most legacy TypeScript projects run tsc --noEmit in isolated, stateless containers without database access. If you run Reylo without a database connection, compilation fails because the content mapper cannot evaluate query signatures.

To configure CI cleanly, spin up an ephemeral container running your database engine (PostgreSQL, MySQL, or SQLite) and run your migration scripts before executing the typecheck command.

# .github/workflows/typecheck.yml
jobs:
  typecheck:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_DB: test_db
          POSTGRES_PASSWORD: password
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
      - run: pnpm install
      - run: pnpm run db:migrate
        env:
          DATABASE_URL: postgres://postgres:password@localhost:5432/test_db
      - run: pnpm run typecheck
        env:
          DATABASE_URL: postgres://postgres:password@localhost:5432/test_db

Handling Schema Drift

A critical failure mode occurs when local developer schemas fall out of sync with production deployments. If a developer drops a table locally or alters a column type without running migration scripts, Reylo instantly flags the affected .sql file import as a compilation error.

This feedback loop prevents broken queries from creeping into deployment packages, catching schema drift directly inside your code editor long before unit tests execute.

Frequently Asked Questions

How to get typed sql in typescript without codegen?

To get typed SQL without build-step codegen, use Reylo with TypeScript 7.1 content mappers. Configure contentMappers in tsconfig.json, connect to a target dev database, and import .sql files directly into your TypeScript project. The engine infers query types dynamically during tsc compilation passes.

Does Reylo execute queries during compilation type checks?

No. Reylo uses database-level query preparation commands (such as PostgreSQL prepared statements) to extract column structures, returned datatypes, and parameter metadata without executing actual SQL statements or mutating database tables.

Which database engines work with Reylo?

Reylo supports PostgreSQL (via pg, postgres.js, or @vercel/postgres), MySQL (via mysql2 and PlanetScale), and SQLite (via better-sqlite3, libsql, or Cloudflare D1). You bring your own client driver.

Why does Reylo require TypeScript 7.1?

Reylo depends on the native contentMappers API introduced in TypeScript 7.1. Content mappers allow third-party packages to dynamically supply type definitions for custom non-TypeScript file extensions during compiler execution.

Action Plan

Integrating native SQL imports eliminates entire classes of build step synchronization errors and frees your team from fragile code generation tools. Install TypeScript 7.1, point Reylo at a local shadow database, and convert your most complex query into a native imported asset today.

Pass this guide to team members wrestling with ORM performance bottlenecks, and read our breakdown of modern database migration patterns to keep your dev schemas synchronized across environments.