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.
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
);// 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;
}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;
}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.
CREATE TYPE payment_status AS ENUM (
'pending', 'authorized', 'captured', 'refunded', 'failed'
);
CREATE TABLE payments (
status payment_status NOT NULL,
...
);// 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;
// ...
}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
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;
}
}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."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.
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.