SQL DDL·TypeScript·Free

SQL DDL to
TypeScript.

Paste CREATE TABLE statements, get typed TypeScript interfaces. Postgres ENUM types become literal unions. Nullable columns land as ?: T | null. Timestamps as Date. One file per table. No ORM, no runtime — just types you can use with any driver.

Postgres·MySQL·Types only·MIT
schema.sqlInput
CREATE TYPE payment_status AS ENUM (
  'pending', 'authorized', 'captured', 'refunded', 'failed'
);

CREATE TABLE payments (
  id            UUID PRIMARY KEY,
  customer_id   UUID NOT NULL,
  amount_cents  INTEGER NOT NULL,
  currency      CHAR(3) NOT NULL,
  status        payment_status NOT NULL,
  description   TEXT,
  captured_at   TIMESTAMP,
  created_at    TIMESTAMP NOT NULL
);
compile
src/models/payments.ts tsc cleanOutput
// src/models/payment-status.enum.ts
export type PaymentStatus =
  | 'pending'
  | 'authorized'
  | 'captured'
  | 'refunded'
  | 'failed';

// src/models/payments.ts
import { PaymentStatus } from './payment-status.enum';

export interface Payments {
  id: string;
  customerId: string;
  amountCents: number;
  currency: string;
  status: PaymentStatus;
  createdAt: Date;
  description?: string | null;
  capturedAt?: Date | null;
}
What gets generated

Plain interfaces. One file per table.

Below: TypeScript emitted from a small Postgres schema with an ENUM, a foreign key, and a nullable column. Navigation properties are opt-in — enable the flag and you get typed parent and child references.

// src/models/payment-status.enum.ts
export type PaymentStatus =
  | 'pending'
  | 'authorized'
  | 'captured'
  | 'refunded'
  | 'failed';

// src/models/customers.ts
import { Orders } from './orders';

export interface Customers {
  id: string;
  email: string;
  createdAt: Date;
  orders: Array<Orders>;
  displayName?: string | null;
}

// src/models/orders.ts
import { OrderState } from './order-state.enum';
import { OrderLines } from './order-lines';
import { Customers } from './customers';

export interface Orders {
  id: string;
  customerId: string;
  state: OrderState;
  totalCents: number;
  placedAt: Date;
  orderLines: Array<OrderLines>;
  notes?: string | null;
  shippedAt?: Date | null;
  customer?: Customers;
}
Three things we get right

Grounded in how the DDL reads.

The SQL-to-TypeScript space is mostly ORM-adjacent — bring the ORM, get the types. This is the inverse: types only, no runtime, no query builder. Each claim here is something you can verify by running the converter on your own schema.

01ProofENUM types become literal unions.Spec · InCode · Out
schema.sql fragment
CREATE TYPE payment_status AS ENUM (
  'pending', 'authorized', 'captured', 'refunded', 'failed'
);

CREATE TABLE payments (
  status payment_status NOT NULL,
  ...
);
emitted .ts files
// payment-status.enum.ts
export type PaymentStatus =
  | 'pending'
  | 'authorized'
  | 'captured'
  | 'refunded'
  | 'failed';

// payments.ts — the column is typed by the union, not `string`.
import { PaymentStatus } from './payment-status.enum';

export interface Payments {
  status: PaymentStatus;
  // ...
}
Node.js · node-postgres

Types for the queries you already write.

The generator emits interfaces; you write the SQL. Pass the emitted type as the row generic on pool.query and every column lands typed — including Postgres enums as literal unions and nullable columns as optional + nullable.

src/db/payment-repo.ts generated
// src/db/payment-repo.ts
import { Pool } from 'pg';
import { Payments } from '../models/payments';
import { PaymentStatus } from '../models/payment-status.enum';

// Types only — the generator never writes SQL. You do.
// Here: the emitted interface types a hand-written query.
export class PaymentRepo {
  constructor(private readonly pool: Pool) {}

  async findById(id: string): Promise<Payments | null> {
    const { rows } = await this.pool.query<Payments>(
      `SELECT
         id,
         customer_id    AS "customerId",
         amount_cents   AS "amountCents",
         currency,
         status,
         description,
         captured_at    AS "capturedAt",
         created_at     AS "createdAt"
       FROM payments WHERE id = $1`,
      [id],
    );
    return rows[0] ?? null;
  }

  async findByStatus(status: PaymentStatus): Promise<Payments[]> {
    const { rows } = await this.pool.query<Payments>(
      'SELECT ... FROM payments WHERE status = $1',
      [status],
    );
    return rows;
  }
}
Also available via MCP

Or ask Claude to do it.

The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a DDL file and it produces the TypeScript interfaces as a diff — free, no token, no leaving the chat.

// claude-desktop config · .mcp.json
{
  "mcpServers": {
    "metaengine": {
      "command": "npx",
      "args": ["-y", "@metaengine/mcp-server"]
    }
  }
}

// Then in Claude:
// "Load db/schema.sql and generate TypeScript
//  models with navigation properties."
See the MCP page
Questions

Things people ask.

Most SQL-to-TypeScript tools are ORM-adjacent. This one is not. The questions below reflect that — if yours is not here, run the converter and see.

Plain TypeScript interfaces — one file per table, plus one file per Postgres ENUM type. Columns map to typed fields. That is it: no repository class, no query builder, no runtime dependency.

Try it on your own schema.

The converter runs in the browser. Your DDL never leaves the page.

Open the converter