I've been using a fork of schemats for a while, it is a great tool.<p>We quickly figured that using the table definitions as our app layer models wouldn't cut it, notably to handle polymorphism and non ideal (sic) table design.<p>We decided to "hand write" (mostly codegen) the types we use in the business logic and have a translation layer between those and the interfaces generated by schemats. For simple tables the mapping is 1:1, but for complicated tables we centralize type checks and defensive assertions there. For polymorphism it would look like this:<p><pre><code> CREATE TYPE invoice_line_type AS ENUM('product', 'shipment');
CREATE TABLE invoice_lines AS (
id SERIAL PRIMARY KEY,
type invoice_line_type NOT NULL,
product_id INT REFERENCES products(id),
shipment_id INT REFERENCES shipments(id),
amount NUMERIC NOT NULL,
CHECK (CASE WHEN type = 'product' THEN product_id IS NOT NULL ELSE TRUE END),
CHECK (CASE WHEN type = 'shipment' THEN shipment_id IS NOT NULL ELSE TRUE END)
);
</code></pre>
The translation layer:<p><pre><code> import { invoice_lines, invoice_line_type } from 'src/schema'
export type ProductInvoiceLine = {
id: number
type: invoice_line_type.product
product_id: number
amount: number
}
export type ShipmentInvoiceLine = {
id: number
type: invoice_line_type.shipment
shipment_id: number
amount: number
}
export const parse = (row: invoice_lines): ProductInvoiceLine | ShipmentInvoiceLine => {
const base = {
id: row.id,
amount: row.amount,
}
switch (row.type) {
case invoice_line_type.product:
return {
...base,
type: invoice_line_type.product,
product_id: ensureNumber(row.product_id),
}
case invoice_line_type.shipment:
return {
...base,
type: invoice_line_type.shipment,
shipment_id: ensureNumber(row.shipment_id),
}
}
}</code></pre>