Skip to content
NewIn search of the type-safest ORM4 min read

Type safety, compared

This is what a mistake or typo costs you per ORM. Ten ordinary mistakes - a misspelled key, a text operator on a number, a read of a column you did not select - written in each ORM’s own API against the same two entities.

The editor below runs the real TS compiler over the real packages you would install.

Switch ORM tab to see what each caught; the more red lines the better below.

type-safety/drizzle.tsstatic preview
/**
* Drizzle through its relational query API (`db.query.*`), not the `db.select()` builder the timed read
* uses: it is the one of the two with a projection, a filter and a sort in one object, so the mistakes
* below are the same mistakes the other five entries make rather than five different ones.
*/
import { eq } from 'drizzle-orm';
import { drizzleUsers } from '../src/schema';
import { clients } from './clients';
const { drizzleDb: db } = clients;
// Misspelled column in the projection | emial -> email
await db.query.User.findMany({
columns: { id: true, emial: true },
});
// Misspelled column in the filter | companyid -> companyId
await db.query.User.findMany({
columns: { id: true },
where: (t, { gt }) => gt(t.companyid, 0),
});
// String value against a numeric column | 'one' -> 1
await db.query.User.findMany({
columns: { id: true },
where: (t, { eq }) => eq(t.companyId, 'one'),
});
// Text operator against a numeric column | op.like(t.companyId, 'abc') -> op.gte(t.companyId, 1)
await db.query.User.findMany({
columns: { id: true },
where: (t, op) => op.like(t.companyId, 'abc'),
});
// Misspelled column in the sort | t.idd -> t.id
await db.query.User.findMany({
columns: { id: true },
orderBy: (t, { asc }) => asc(t.idd),
});
// Misspelled column inside a loaded relation | nmae -> name
await db.query.Company.findMany({
columns: { id: true },
with: { users: { columns: { nmae: true } } },
});
// Misspelled column in inserted data | emails -> email
await db.insert(drizzleUsers).values([{ name: 'New User', emails: 'new@example.com' }]);
// Number written into a text column | 42 -> 'Updated Name'
await db.update(drizzleUsers).set({ name: 42 }).where(eq(drizzleUsers.id, 1));
// Reading a column the projection left out | user.email -> user.name
const [user] = await db.query.User.findMany({ columns: { id: true, name: true } });
export const unselected = user.email;
// Reading a misspelled column off a loaded relation | .nmae -> .name
const [company] = await db.query.Company.findMany({
columns: { id: true },
with: { users: { columns: { id: true, name: true } } },
});
export const nested = company.users[0].nmae;
Scored with TypeScript 7.0.2 in the benchmark; the editor runs 6.0.3, this repository's own compiler rather than the 5.9 Monaco vendors. The build refuses to deploy a page whose squiggles do not match the scores above.

The files above come from ts-orm-benchmark, the repository behind the speed benchmark, and this site changes nothing in them. Each ORM’s ten queries live in a file of its own there and are compiled twice: once as written, once with every mistake corrected. A mistake counts as caught only when it errors and the corrected copy is clean, so nothing scores a point for being broken in a way that has nothing to do with the probe.

That is the only place anything is scored. This site vendors those six files with the verdicts they produced, compiles them again at build the way the browser will, and fails to deploy if the editor would disagree.

Ten probes cannot separate six ORMs the way a microsecond can. UQL catches all ten and three others catch nine, which is one probe of daylight and not a gap you should choose a tool over; the honest reading is that five of these six check almost everything an ordinary mistake can be, and one does not.

Nothing here covers migrations, tooling, how legible an error is when it does fire, or whether the types stay fast on a schema with two hundred tables. It is one axis, picked because it is the one people argue about without evidence.

Each entry is written in the one API its version actually offers: MikroORM 7 ships no entity decorators at all, and TypeORM’s are still the legacy kind, so its entities use EntitySchema - which scores the same either way. Drizzle is queried through db.query.* rather than db.select(), because it is the one of the two that takes a projection, a filter and a sort in a single object, which is what the other five are being asked for.

I wrote UQL, and it comes out ahead here by one probe. Take that for what it is worth from its author: the method is in the open, the ten probes are the same for every entry, and the editor above is running the compiler rather than showing you my screenshot of it.