ORM Type Safety Comparison
The same ordinary mistakes, written in six ORMs’ own APIs against the same two entities: a misspelled key, a text operator on a number, a sum over a text column, a read of a column you never selected, and eight more. The editor runs the real TypeScript compiler over the real packages. Switch tabs to see what each one catches.
import { asc, eq, gt, like, sum } from 'drizzle-orm';import { drizzleUsers } from '../src/schema';import { clients } from './clients';
const { drizzleDb: db } = clients;
// Misspelled column in the projection | emial -> emailawait db .select({ id: drizzleUsers.id, email: drizzleUsers.emial }) .from(drizzleUsers);
// Misspelled column in the filter | createdat -> createdAtawait db .select({ id: drizzleUsers.id }) .from(drizzleUsers) .where(gt(drizzleUsers.createdat, 0));
// String value against a numeric column | 'one' -> 1await db .select({ id: drizzleUsers.id }) .from(drizzleUsers) .where(eq(drizzleUsers.createdAt, 'one'));
// Text operator against a numeric column | like(drizzleUsers.createdAt, 'abc') -> gt(drizzleUsers.createdAt, 1)await db .select({ id: drizzleUsers.id }) .from(drizzleUsers) .where(like(drizzleUsers.createdAt, 'abc'));
// Misspelled column in the sort | idd -> idawait db .select({ id: drizzleUsers.id }) .from(drizzleUsers) .orderBy(asc(drizzleUsers.idd));
// Sum over a text column | name -> idawait db.select({ total: sum(drizzleUsers.name) }).from(drizzleUsers);
// Misspelled column inside a loaded relation | nmae -> nameawait db.query.Company.findMany({ columns: { id: true }, with: { users: { columns: { nmae: true } } },});
// Misspelled column in inserted data | emails -> emailawait db .insert(drizzleUsers) .values([{ name: 'New User', emails: 'new@example.com' }]);
// Required column left out of inserted data | 'New User' } -> 'New User', email: 'new@example.com' }await db.insert(drizzleUsers).values([{ name: 'New User' }]);
// 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.nameconst [user] = await db .select({ id: drizzleUsers.id, name: drizzleUsers.name }) .from(drizzleUsers);export const unselected = user.email;
// Reading a misspelled column off a loaded relation | .nmae -> .nameconst [company] = await db.query.Company.findMany({ columns: { id: true }, with: { users: { columns: { id: true, name: true } } },});export const nested = company.users[0].nmae;How it is scored
Section titled “How it is scored”The files come unchanged from ts-orm-benchmark, which also runs the speed benchmark. Each ORM’s file compiles twice: as written, and with every mistake fixed. A mistake counts as caught only when it errors and the fixed version compiles clean.
The benchmark scores with TypeScript 7.0.2. This site recompiles the same files at build time with TypeScript 6.0.3, the compiler the editor runs too, and the deploy fails if a verdict changes.
Each entry uses the API its main package offers. MikroORM’s entities use defineEntity, since its decorators moved
to @mikro-orm/decorators. TypeORM’s decorators are the legacy kind, so its entities use EntitySchema, which
scores the same. Drizzle’s flat probes use db.select() and its relation probes db.query.*, the same code its
benchmark times.
Reading the result
Section titled “Reading the result”UQL catches all twelve, Prisma eleven, Drizzle, MikroORM and TypeORM ten, and Sequelize six. A one-probe gap should not decide a tool on its own, and this page says nothing about migrations, error readability or type-checking speed on large schemas.
I wrote UQL, so weigh the verdict accordingly. The method is open, and every entry faces the same probes.
Try it
npm install uql-ormEntities are plain classes and queries are plain JSON: nothing to generate, and no schema file to keep in sync.