Skip to content

MySQL quickstart

This path takes a small MySQL application from an existing database connection to one checked and executed query. The grammar and compiler are stable; the optional editor integration is not part of this path.

1. Check the prerequisites

Use Node.js 22.11 or newer, TypeScript 7.0.2, and a MySQL 8.4 or 9.7 LTS database. Review MySQL compatibility for exact tested targets and SQL-mode boundaries.

Create an empty database named app, then set its connection URI:

sh
export DATABASE_URL=mysql://user:password@127.0.0.1:3306/app

The database and its lifecycle remain yours.

Start an empty ESM project:

sh
mkdir typed-sql-mysql && cd typed-sql-mysql
pnpm init
pnpm pkg set type=module
mkdir src

2. Install the packages

sh
pnpm add @typed-sql/core @typed-sql/mysql mysql2
pnpm add -D @typed-sql/cli typescript tsx

mysql2 is an explicit application dependency. Installing @typed-sql/mysql does not install it.

Create tsconfig.json:

json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2024",
    "strict": true,
    "noEmit": true
  },
  "include": ["src/**/*.ts", "typed-sql.config.ts"]
}

3. Create a minimal table

Create src/setup.ts:

ts
import { createConnection } from "mysql2/promise";

const connection = await createConnection(process.env.DATABASE_URL!);

try {
  await connection.query(`
    CREATE TABLE users (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
      email VARCHAR(255) NOT NULL UNIQUE,
      status ENUM('active', 'suspended') NOT NULL DEFAULT 'active'
    )
  `);
  await connection.query("INSERT INTO users (email) VALUES (?)", ["ada@example.com"]);
} finally {
  await connection.end();
}

Run it once against the empty database:

sh
pnpm exec tsx src/setup.ts

4. Create a minimal config

Create typed-sql.config.ts:

ts
import { defineConfig } from "@typed-sql/core";
import { mysql, typePolicy } from "@typed-sql/mysql";
import { mysql2 } from "@typed-sql/mysql/mysql2";

const connectionUri = () => process.env.DATABASE_URL!;

export default defineConfig({
  dialect: mysql({ typePolicy }),
  schema: {
    file: "generated/db/schema.json",
    provider: mysql2({ connectionUri, schemas: ["app"], typePolicy }),
  },
  outDir: "generated/db",
  projects: ["tsconfig.json"],
  typePolicy,
});

The same typePolicy controls introspection, compile-time inference, and adapter decoding.

5. Generate the snapshot

sh
pnpm exec typed-sql generate

This introspects MySQL and writes deterministic compiler input under generated/db. Commit the snapshot so schema changes are reviewable; application code does not import it.

6. Write one parameterized query

Create src/account.ts:

ts
import { sql } from "@typed-sql/mysql";

export const accountById = (accountId: bigint) => sql`
  SELECT account.id, account.email, account.status
  FROM users AS account
  WHERE account.id = ${accountId}
`;

accountId remains a value segment and renders as ?.

7. Check the inferred contract

sh
pnpm exec typed-sql check --project tsconfig.json

Against the table above, the compiler proves:

ts
type AccountByIdQuery = Query<
  { "id": bigint; "email": string; "status": "active" | "suspended"; },
  readonly [bigint]
>;

The stable check is authoritative. An ordinary TypeScript server can still show the conservative published Query<unknown> declaration; compiler and editor workflow explains why.

8. Execute the query

Create src/run.ts:

ts
import { typePolicy } from "@typed-sql/mysql";
import { createMySql2Database } from "@typed-sql/mysql/mysql2";
import { accountById } from "./account.js";

const database = await createMySql2Database({
  connectionUri: process.env.DATABASE_URL!,
  typePolicy,
});

try {
  console.log(await database.maybeOne(accountById(1n)));
} finally {
  await database.close();
}
sh
pnpm exec tsx src/run.ts

The adapter is optional. You can instead render into an existing mysql2 pool.

9. Confirm a type error

Temporarily interpolate a string directly in the BIGINT comparison:

ts
const wrongId = "1";

sql`SELECT id FROM users WHERE id = ${wrongId}`;

Run typed-sql check again. The transformed check reports that string is not assignable to the bigint parameter position. Restore the valid query before continuing.

10. Choose the next step

What just happened?

  • The MySQL grammar—not the neutral compiler—owned SQL syntax, enum inference, placeholders, modes, and diagnostics.
  • The generated snapshot supplied schema evidence without becoming an application API.
  • typed-sql check proved the row and ordered parameter tuple and remains the authoritative result.
  • The interpolated id stayed a driver parameter instead of becoming SQL text.
  • The shared type policy kept inferred types and runtime decoding aligned; no runtime validation was implied.
  • The application installed and closed mysql2; typed-sql did not own the driver or connection lifecycle.

Released under the MIT License.