TypeORM (TypeScript / JavaScript)
A decorator-based, code-first ORM for TypeScript and JavaScript whose distinguishing trait is that one entity model — a class annotated with @Entity / @Column / relation decorators — drives two persistence styles: the Active Record pattern (class User extends BaseEntity → User.findOneBy(...), user.save()) and the Data Mapper pattern (dataSource.getRepository(User) → repo.save(user)).
| Field | Value |
|---|---|
| Language | TypeScript / JavaScript (ES2023+), Node.js + browser/Cordova/Ionic/React Native/Expo/Electron |
| License | MIT (LICENSE; package.json "license") |
| Repository | typeorm/typeorm |
| Documentation | typeorm.io · docs/ in-repo |
| Category | Full ORM supporting both Active Record and Data Mapper patterns — decorator-based, code-first |
| Abstraction level | Full-ORM rung: entities with declared relations, per-save change diffing, cascades, migrations |
| Query model | Decorators + repository find-options · fluent QueryBuilder · raw SQL / tagged template — a runtime value rendered to dialect SQL at execution |
| Effect/async model | Async (Promise / async); failures are thrown (QueryFailedError / TypeORMError), not a typed error channel |
| Backends | PostgreSQL, MySQL/MariaDB, CockroachDB, SQLite (better-sqlite3 / sql.js), MS SQL Server, Oracle, SAP HANA, Google Spanner, MongoDB (DataSourceOptions) |
| First release | 0.0.1 ≈ 2016 (web-attested; CHANGELOG.md: "first stable version, works with TypeScript 1.x") |
| Latest version | 1.0.0 @ 2026-05-19 (the pinned tree; CHANGELOG.md top entry — the release that finished the Connection → DataSource rename) |
NOTE
TypeORM is this survey's data point for a decorator-driven full ORM that lets one entity model be used two ways. Where Prisma puts the schema in a separate .prisma DSL and generates a client, and Drizzle / Kysely expose typed builders with no decorators, TypeORM's model is ordinary TypeScript classes tagged with metadata decorators (reflect-metadata reads the emitted types). It sits at the full-ORM rung alongside Hibernate, SQLAlchemy, and Rails ActiveRecord — with change tracking, cascades, lazy relations, and a migration runner — but stacks the Active Record and Data Mapper surfaces on the same metadata. Terms below link to concepts.
Overview
What it solves
TypeORM maps SQL tables to typed TypeScript classes and lets you query and mutate them through a repository/entity-manager API (Data Mapper) or through methods on the entity itself (Active Record). Its scope is deliberately maximal — the README positions it as a run-anywhere, do-everything ORM (README.md):
"TypeORM is an ORM that can run in Node.js, Browser, Cordova, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES2023). Its goal is to always support the latest JavaScript features and provide additional features that help you to develop any kind of application that uses databases - from small applications with a few tables to large-scale enterprise applications with multiple databases."
The package.json description names the lineage and the breadth of backends (package.json):
"Data-Mapper ORM for TypeScript and ES2023+. Supports MySQL/MariaDB, PostgreSQL, MS SQL Server, Oracle, SAP HANA, SQLite, MongoDB databases."
The README names its influences directly — "TypeORM is highly influenced by other ORMs, such as Hibernate, Doctrine and Entity Framework" (README.md) — so the mental model is Hibernate/JPA, not a thin query builder.
Design philosophy
The single claim TypeORM leads with — the one the rest of the library is organized around — is that it offers two enterprise patterns off one model (README.md):
"TypeORM supports both Active Record and Data Mapper patterns, unlike all other JavaScript ORMs currently in existence, which means you can write high-quality, loosely coupled, scalable, maintainable applications in the most productive way."
Both are grounded in the same decorated class. The README shows the Data Mapper form — a plain entity plus a repository (README.md):
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
}
const userRepository = MyDataSource.getRepository(User);
await userRepository.save(user);
const allUsers = await userRepository.find();
const firstUser = await userRepository.findOneBy({ id: 1 });and the Active Record form — the same shape, but extends BaseEntity, so persistence methods live on the class (README.md):
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm';
@Entity()
export class User extends BaseEntity {
/* @PrimaryGeneratedColumn / @Column … */
}
await user.save();
const allUsers = await User.find();
const timber = await User.findOneBy({ firstName: 'Timber', lastName: 'Saw' });The guide frames the choice as a maintainability-vs-simplicity trade, not a technical one (docs/docs/guides/1-active-record-data-mapper.md): "The Data Mapper approach helps with maintainability, which is more effective in larger apps. The Active Record approach helps keep things simple which works well in smaller apps." BaseEntity is literally "Base abstract entity for all entities, used in ActiveRecord patterns" (src/repository/BaseEntity.ts), and every static/instance method on it just delegates to a repository — save(options?) calls baseEntity.getRepository().save(this, options) — so Active Record is a thin veneer over the Data Mapper machinery, not a parallel implementation. This is the whole design in one fact: the two "patterns" share the persistence engine (§ Effect model), a Repository that is "supposed to work with your entity objects" (src/repository/Repository.ts).
The metadata approach is decorator-driven and code-first: decorators push into a global getMetadataArgsStorage() at class-definition time, and TypeORM builds an EntityMetadata graph from that. @Entity marks "classes that will be an entity (table or document depend on database type)" (src/decorator/entity/Entity.ts) and @Column marks "a specific class property as a table column", with the load-bearing caveat that "Only properties decorated with this decorator will be persisted to the database when entity be saved" (src/decorator/columns/Column.ts).
Connection, pooling & resource lifetime
A DataSource is the connection configuration and the root object. Its docstring records the rename that the pinned 1.0.0 finalized (src/data-source/DataSource.ts):
"DataSource is a pre-defined connection configuration to a specific database. You can have multiple data sources connected (with multiple connections in it), connected to multiple databases in your application. Before, it was called
Connection, but nowConnectionis deprecated becauseConnectionisn't the best name for what it's actually is."
You configure it with a DataSourceOptions (a discriminated union over the driver type — PostgresDataSourceOptions, MysqlDataSourceOptions, SqliteDataSourceOptions, … — src/data-source/DataSourceOptions.ts) and open it with await dataSource.initialize(), which builds metadata, connects the driver, optionally runs migrations and/or synchronize, then sets isInitialized = true (src/data-source/DataSource.ts). Pooling is the driver's job, not TypeORM's: the README lists "Connection pooling" and "Replication" among features, and each relational driver wraps its native pool (pg, mysql2, mssql, …, declared as optional peerDependencies in package.json). A QueryRunner is the object that holds a single leased connection for the duration of a unit of work — every transaction, migration, and schema-builder step runs on one; dataSource.query(...) without an explicit runner leases one and releases it in a finally (src/data-source/DataSource.ts). Resource lifetime is therefore callback-scoped (transaction(cb)) or manually managed (queryRunner.release()), not a type-level scoped acquire/release — a leaked QueryRunner is a runtime pool leak, not a compile error. For large result sets, the README advertises "Streaming raw results" over a server-side cursor.
Query construction & injection safety
TypeORM offers three query surfaces over the same entity metadata, in ascending order of control and descending order of abstraction.
1. Repository find-options — the declarative surface. repository.find(options) / findOne(options) / findOneBy(where) take a plain object describing the query. The shape is typed against the entity (src/find-options/FindOneOptions.ts): where is a "Simple condition that should be applied to match entities", relations"Indicates what relations of entity should be loaded (simplified left join form)", plus select, order, take/skip, lock, and cache. Values in a where object are never string-concatenated — they flow into the QueryBuilder below and become bound parameters. The README's canonical example (README.md):
const timber = await userRepository.findOneBy({
firstName: 'Timber',
lastName: 'Saw',
}); // find by firstName and lastNameFindOptionsWhere supports operators (In, LessThan, Like, IsNull, Between, …) that wrap a value as a FindOperator, so even Like("%chocolate%") binds rather than interpolates.
2. The fluent QueryBuilder — the SQL-shaped surface. For joins, sub-queries, and clauses the find-options can't express, createQueryBuilder(alias) returns a SelectQueryBuilder (siblings: Insert/Update/Delete/SoftDelete/Relation) whose methods mirror SQL. It carries named :param placeholders. The active-record guide's findByName shows the idiom verbatim (docs/docs/guides/1-active-record-data-mapper.md):
static findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany()
}where(expr, parameters?) "Sets WHERE condition in the query builder … Additionally you can add parameters used in where expression" (src/query-builder/SelectQueryBuilder.ts). The { firstName } object never enters the SQL text; it is stored in expressionMap.parameters by setParameter, which validates the key against a character allow-list to keep an attacker from smuggling structure through a parameter name (src/query-builder/QueryBuilder.ts):
if (!key.match(/^([A-Za-z0-9_.]+)$/)) {
throw new TypeORMError(
'QueryBuilder parameter keys may only contain numbers, letters, underscores, or periods.',
);
}Values that TypeORM itself injects (a find-option where value, a comparison operand) are captured by createParameter, which mints a fresh orm_param_N name, stores the value, and emits only the placeholder :orm_param_N into the SQL (src/query-builder/QueryBuilder.ts).
Placeholders become bound parameters at execution. getQueryAndParameters() renders the SQL and then calls driver.escapeQueryWithParameters(query, parameters) (src/query-builder/QueryBuilder.ts). Each driver rewrites the named :name placeholders into its native positional form and pulls the values out into an ordered array on a separate channel — the mechanism that makes SQL injection structurally impossible. Postgres, for example, replaces :name with $1, $2, … and appends the values to escapedParameters (src/driver/postgres/PostgresDriver.ts):
sql = sql.replaceAll(/:(\.\.\.)?([A-Za-z0-9_.]+)/g, (full, isArray, key) => {
// …
escapedParameters.push(value);
return this.createParameter(key, escapedParameters.length - 1); // -> "$N"
});The :(\.\.\.)? form (:...ids) expands an array into a placeholder list, so WHERE id IN (:...ids) binds each element rather than string-joining them.
3. Raw SQL — the escape hatch, still parameterized. dataSource.query(sql, params)"Executes raw SQL query and returns raw database results" with a positional/named parameters array (src/data-source/DataSource.ts). For interpolation that stays safe, 1.0.0 adds an sql tagged template whose expressions are auto-bound (src/data-source/DataSource.ts):
"Tagged template function that executes raw SQL query and returns raw database results. Template expressions are automatically transformed into database parameters."
dataSource.sql`SELECT * FROM table_name WHERE id = ${id}`;buildSqlTag walks the template: each ${expr} becomes a driver placeholder via driver.createParameter(...) and the value is pushed onto a parameters array, never the SQL string (src/util/SqlTagUtils.ts). The genuinely unsafe door is building a SQL string yourself and passing it to query() with the value already concatenated in — which no API forces you toward.
Schema, migrations & code generation
TypeORM is code-first: the decorated entities are the schema (schema stances). Two paths turn them into DDL.
Auto-synchronization (development). dataSource.synchronize() (or the synchronize: true option, run during initialize) diffs the entity metadata against the live database and issues the CREATE/ALTER needed to converge, via the schema-builder. The docs and the README are emphatic that this is a dev-only convenience — it can drop columns/data — but it means "Schema declaration in models or separate configuration files" needs no migration for prototyping.
Migrations (production). A migration is a class implementing MigrationInterface with up(queryRunner) / down(queryRunner); MigrationExecutor "Executes migrations: runs pending and reverts previously executed migrations" (src/migration/MigrationExecutor.ts), recording applied ones in a bookkeeping table and wrapping them per a transaction mode (src/migration/MigrationExecutor.ts):
"Indicates how migrations should be run in transactions. all: all migrations are run in a single transaction / none: all migrations are run without a transaction / each: each migration is run in a separate transaction"
The headline feature is generation from the diff: the CLI's migration:generate <path> command asks the schema-builder for the SqlInMemory of upQueries / downQueries needed to bring the DB in line with the entities, and writes them into a migration file — "No changes in database schema were found - cannot generate a migration" when the diff is empty (src/commands/MigrationGenerateCommand.ts). So the workflow is: edit the decorated entities, migration:generate to author the DDL, review, commit. (You can also migration:create an empty migration and write DDL by hand.) This is the same code-first stance as Prisma and EF Core, but sourced from decorated classes rather than a .prisma file or C# model. There is no first-party db-first codegen in-tree; the community typeorm-model-generator fills that gap (README.md).
Type mapping & result decoding
Column types are declared on the decorator (@Column("varchar"), @Column({ type: "jsonb" })), spanning per-database types the driver maps to native encoders/decoders. Nullability is expressed as @Column({ nullable: true }) and reflected in the TypeScript field type (name: string | null). Row hydration is metadata-driven: find/QueryBuilder.getMany() construct entity instances and populate only mapped columns, with relations attached according to the load strategy below. @Column's "only decorated properties persist" rule is the decode-side contract too — an un-decorated field is neither written nor read. Special columns get dedicated decorators (@PrimaryGeneratedColumn for auto-increment/uuid keys, @CreateDateColumn, @UpdateDateColumn, @DeleteDateColumn for soft-delete, @VersionColumn for optimistic locking), and @Column(() => Profile) embeds a value object. Because the mapping leans on reflect-metadata's design:type, the TypeScript compiler must emit decorator metadata (experimentalDecorators + emitDecoratorMetadata), and reflect-metadata must be imported once at startup (docs/docs/getting-started.md) — a hard dependency on that reflection shim is a defining constraint (§ Weaknesses).
Effect model, transactions & error handling
This is the dimension the survey weights most heavily, and the one where TypeORM's mainstream-ORM heritage shows most.
Async Promise, not an effect value. Every terminal operation is async: repository.find(), user.save(), qb.getMany(), dataSource.query() all return Promises awaited by the caller. There is no IO / Effect / ConnectionIO wrapper (effect-typed APIs in the survey's sense) and no type-level error channel — the "effect" is a plain JS promise, and failures are thrown, not returned.
Persistence is whole-graph, diffed against loaded state. save(entity) does far more than a single INSERT/UPDATE; it drives the persistence/ subject executor, which is where TypeORM's Unit of Work-style machinery lives. The pipeline: a Subject "is a subject of persistence … holds information about each entity that needs to be persisted" (src/persistence/Subject.ts); the CascadesSubjectBuilder "Finds all cascade operations of the given subject and cascade operations of the found cascaded subjects, e.g. builds a cascade tree" (src/persistence/subject-builder/CascadesSubjectBuilder.ts), so a save of a User with cascade-flagged relations also persists its Profile and Posts; the SubjectDatabaseEntityLoader then loads each entity's current DB row (src/persistence/SubjectDatabaseEntityLoader.ts):
"Loads database entities for all operate subjects which do not have database entity set. All entities that we load database entities for are marked as updated or inserted. To understand which of them really needs to be inserted or updated we need to load their original representations from the database."
SubjectChangedColumnsComputer then "Finds what columns are changed in the subject entities" — its internal step is documented as "Differentiate columns from the updated entity and entity stored in the database" (src/persistence/SubjectChangedColumnsComputer.ts) — a snapshot diff against the freshly-loaded row, per save call, so an UPDATE touches only changed columns. Finally SubjectExecutor "Executes all database operations (inserts, updated, deletes) that must be executed with given persistence subjects" (src/persistence/SubjectExecutor.ts), ordering them with a SubjectTopologicalSorter that "Orders insert or remove subjects in proper order (using topological sorting)" so FK dependencies are satisfied (src/persistence/SubjectTopologicalSorter.ts). This is change tracking realized as load-then-diff at save time rather than a long-lived session snapshot: TypeORM has no per-session identity map that would return the same instance for two loads, and no ambient flush — the "unit of work" is the graph reachable from one save() call. BaseEntity.save documents the upsert semantics directly: "Saves current entity in the database. If entity does not exist in the database then inserts, otherwise updates" (src/repository/BaseEntity.ts).
Transactions: callback or manual, nested via SAVEPOINT. dataSource.transaction(cb)"Wraps given function execution (and all operations made there) into a transaction. All database operations must be executed using provided entity manager" (src/data-source/DataSource.ts). The EntityManager implementation opens a QueryRunner, startTransaction, runs the callback, commits on success and rolls back on a thrown error (src/entity-manager/EntityManager.ts):
await queryRunner.startTransaction(isolation);
const result = await runInTransaction(queryRunner.manager);
await queryRunner.commitTransaction();
// catch: await queryRunner.rollbackTransaction(); throw errAn optional first argument sets the isolation level. Nesting is real and implemented with savepoints: the query runner tracks a transactionDepth, so an outer transaction emits START TRANSACTION while an inner one emits SAVEPOINT typeorm_N, with commit/rollback mapping to RELEASE SAVEPOINT / ROLLBACK TO SAVEPOINT (src/driver/postgres/PostgresQueryRunner.ts):
if (this.transactionDepth === 0) {
await this.query('START TRANSACTION');
// SET TRANSACTION ISOLATION LEVEL …
} else {
await this.query(`SAVEPOINT typeorm_${this.transactionDepth}`);
}
this.transactionDepth += 1;so an inner rollback discards only the inner work while the outer transaction survives.
Errors are thrown exceptions, not a typed channel. A failed query throws a QueryFailedError — "Thrown when query execution has failed" — which carries the query, the parameters, and the raw driverError (src/error/QueryFailedError.ts); a missing row from findOneOrFail throws EntityNotFoundError; optimistic-lock conflicts throw OptimisticLockVersionMismatchError; everything derives from TypeORMError (src/error/TypeORMError.ts). There is no Result/Either and no isRetryable-style classification — to distinguish a unique-key violation from a deadlock you inspect error.driverError (the underlying pg/mysql2 error and its SQLSTATE code) yourself. This is the ordinary exception posture of the mainstream ORMs (Hibernate, EF Core), the opposite end of the axis from the functional data mappers this survey weights (doobie, Effect TS), which keep the failure set in the effect's type.
Relations & the N+1 hazard. Relations are declared with @ManyToOne / @OneToMany / @OneToOne / @ManyToMany plus @JoinColumn / @JoinTable. @ManyToOne documents the ownership rule — "Entity1 is the owner of the relationship, and stores the id of Entity2 on its side of the relation" (src/decorator/relations/ManyToOne.ts). Loading has three modes, and the choice is where N+1 lurks:
- Eager via join —
find({ relations: { posts: true } })(the "simplified left join form" ofFindOneOptions) orqb.leftJoinAndSelect("post.category", "category")pulls the relation in one query.FindOneOptionseven exposesrelationLoadStrategyto pick "join" (one query with joins) vs "query" (a separateWHERE … INper relation) — "If you are loading too much data with nested joins it's better to load relations using separate queries" (src/find-options/FindOneOptions.ts). - Lazy
Promise-typed relations — if a relation property is typedPromise<Post[]>, TypeORM auto-marks it lazy (OneToManyinspects the reflecteddesign:typeand setsisLazywhen the type name ispromise—src/decorator/relations/OneToMany.ts); theRelationLoaderthen "provides lazy-load wrappers via getters/setters" (src/query-builder/RelationLoader.ts), so touchingawait user.postsfires a query. Convenient, and the classic N+1 foot-gun: iterating N users and awaiting eachuser.postsis N+1 round-trips. RelationId/ relation counts — load only the foreign keys or counts without the full rows.
Ecosystem & maturity
TypeORM is one of the most-depended-upon ORMs in the Node ecosystem — the default ORM in much of the NestJS world — released under the permissive MIT license (LICENSE). It supports "more databases than any other JS/TS ORM" (README.md): PostgreSQL, MySQL/MariaDB, CockroachDB, SQLite (via sqlite3, better-sqlite3, sql.js, Capacitor, Cordova, Expo, React Native, NativeScript), MS SQL Server, Oracle, SAP HANA, Google Spanner, and MongoDB (as a document store) — the DataSourceOptions union enumerates all of them (src/data-source/DataSourceOptions.ts), and the database packages are optional peerDependencies (package.json). The feature surface is enormous: the README's feature list runs to ~40 bullets — eager/lazy relations, cascades, closure-table trees, multiple inheritance patterns, replication, cross-database queries, query caching, listeners/subscribers (hooks), a CLI, ESM + CommonJS. A community-extension ecosystem surrounds it (typeorm-model-generator, typeorm-extension, fixtures loaders, ER-diagram generators — README.md). First released as 0.0.1 around 2016 ("first stable version, works with TypeScript 1.x", CHANGELOG.md); the project spent years on the 0.3.x line and the pinned tree is 1.0.0 (2026-05-19) — the release that dropped Node 16/18 and finished renaming Connection to DataSource (CHANGELOG.md).
Strengths
- One model, two patterns. Active Record and Data Mapper over the same decorated entity; the AR surface is a thin
BaseEntityveneer over the repository engine (src/repository/BaseEntity.ts). - Code-first with migration generation. Edit entities,
migration:generatediffs them against the DB and writes the DDL (src/commands/MigrationGenerateCommand.ts);synchronizefor prototyping. - Injection-safe across all three surfaces. Find-option values,
QueryBuilder:params, and thesqltagged template all bind out-of-band;escapeQueryWithParameterssplits SQL from data (src/driver/postgres/PostgresDriver.ts,src/util/SqlTagUtils.ts). - Whole-graph persistence.
savecascades, loads current rows, diffs changed columns, and topologically orders writes — a real unit-of-work per call (src/persistence/SubjectExecutor.ts). - Broadest backend coverage of any JS/TS ORM, including MongoDB, across a dozen runtimes (
src/data-source/DataSourceOptions.ts). - Real nested transactions.
SAVEPOINT-based nesting with isolation levels (src/driver/postgres/PostgresQueryRunner.ts). - Rich
QueryBuilderescape hatch for anything find-options can't express, still parameterized (src/query-builder/SelectQueryBuilder.ts).
Weaknesses
- Runtime, not compile-time, query checking. A
wherereferencing a non-existent column, or aQueryBuilderstring typo, is a runtimeEntityPropertyNotFoundError/QueryFailedError, not a type error — unlikeKysely/Drizzle's typed builders orPrisma's generated client. reflect-metadata+ decorator config dependency. Requires thereflect-metadatashim imported at startup andexperimentalDecorators/emitDecoratorMetadataintsconfig(docs/docs/getting-started.md) — friction with bundlers, SWC/esbuild, and the newer TC39 decorators.- No typed error channel. Failures are thrown
TypeORMErrorsubclasses; distinguishing a unique violation from a deadlock means reaching intoerror.driverError(src/error/QueryFailedError.ts) — no encoded, retryable-aware error set. - Lazy
Promiserelations are an N+1 trap. Auto-lazy onPromise-typed properties (src/decorator/relations/OneToMany.ts) makes per-parent queries easy to trigger accidentally. - Large, sometimes rough, feature surface. ~40 headline features and many overlapping APIs (find-options vs
QueryBuildervs raw) mean more to learn and more edge cases; theQueryBuildersource still carries a long// todo:list (src/query-builder/QueryBuilder.ts). - No first-party db-first codegen. Scaffolding entities from a live schema needs a community tool (
README.md). savesemantics can surprise. The load-diff-cascade pipeline issues extraSELECTs and can cascade further than intended ifcascadeflags are broad (src/persistence/subject-builder/CascadesSubjectBuilder.ts).
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Decorator-based, code-first entities via reflect-metadata | Model is plain TS classes; schema, columns, relations declared inline; no separate DSL | Hard dependency on reflect-metadata + experimentalDecorators; no compile-time query verification |
| Both Active Record and Data Mapper from one entity | Familiar to Hibernate/Rails users; pick per-app-size; AR is a BaseEntity veneer | Two surfaces to document/learn; AR couples domain objects to persistence |
Three query surfaces (find-options · QueryBuilder · raw/sql) | Declarative for the common case, fluent for joins, raw for the rest — all parameterized | Overlapping APIs; when to reach for which is a judgment call; find-options can't express everything |
:param placeholders → escapeQueryWithParameters per driver | Injection impossible for values; SQL and data on separate channels; multi-dialect | You must pass values as params, never concatenate; the one unsafe door is hand-built raw SQL |
save = load current row, diff changed columns, cascade, topo-sort | A real unit-of-work per call; UPDATEs touch only changed columns; graph persistence | Extra SELECTs per save; no session identity map; cascades can reach further than expected |
Async Promise + thrown errors | Idiomatic JS; integrates with async/await everywhere | No effect value, no typed/retryable error channel (effect-first); classification needs driver error |
Lazy relations via Promise-typed properties | Ergonomic on-demand loading with plain await | Classic N+1 foot-gun; loading strategy must be chosen deliberately |
| Code-first migrations generated from the entity↔DB diff | Single source of truth in the models; migration:generate authors DDL for you | Generated SQL needs review; no in-tree db-first scaffolding; synchronize is dev-only |
Sources
- typeorm/typeorm — GitHub repository · typeorm.io — official docs ·
docs/in-repo README.md— positioning, "supports both Active Record and Data Mapper", influences, entity/AR/DM examples, backend list, feature list, extensionspackage.json— MIT license,"Data-Mapper ORM for TypeScript and ES2023+", optional databasepeerDependenciessrc/decorator/entity/Entity.ts—@Entitymetadata decorator ·src/decorator/columns/Column.ts—@Column("only decorated properties persist") ·src/decorator/columns/PrimaryGeneratedColumn.tssrc/decorator/relations/ManyToOne.ts— relation ownership ·src/decorator/relations/OneToMany.ts— auto-lazy onPromisetypesrc/repository/BaseEntity.ts— Active Record base;save= insert-or-update; delegates to repository ·src/repository/Repository.ts— Data Mapper repositorysrc/data-source/DataSource.ts—DataSource(formerlyConnection);initialize,transaction,query,sqltagged template,createQueryBuilder,synchronize·src/data-source/DataSourceOptions.ts— backend unionsrc/entity-manager/EntityManager.ts—transaction(open runner, commit/rollback),save→EntityPersistExecutorsrc/query-builder/SelectQueryBuilder.ts— fluent builder;where(expr, params),leftJoinAndSelect,getMany·src/query-builder/QueryBuilder.ts—setParameterkey allow-list,createParameter(:orm_param_N),escapeQueryWithParameterssrc/driver/postgres/PostgresDriver.ts—escapeQueryWithParameters::name→$N, values out-of-band,:...arrexpansion ·src/driver/postgres/PostgresQueryRunner.ts—START TRANSACTIONvsSAVEPOINT typeorm_NbytransactionDepthsrc/util/SqlTagUtils.ts—buildSqlTag: template expressions → bound parameterssrc/find-options/FindOneOptions.ts—where/relations/relationLoadStrategy("join" vs "query")src/persistence/Subject.ts— a persistence subject ·src/persistence/SubjectDatabaseEntityLoader.ts— load current DB rows to diff ·src/persistence/SubjectChangedColumnsComputer.ts— diff changed columns vs DB entity ·src/persistence/SubjectExecutor.ts— execute inserts/updates/deletes ·src/persistence/SubjectTopologicalSorter.ts— FK-safe ordering ·src/persistence/subject-builder/CascadesSubjectBuilder.ts— cascade treesrc/migration/MigrationExecutor.ts— run/revert migrations,transaction"all"/"none"/"each" ·src/commands/MigrationGenerateCommand.ts—migration:generatefrom the entity↔DB diffsrc/query-builder/RelationLoader.ts— lazy-load wrapperssrc/error/QueryFailedError.ts— thrown on query failure, carriesdriverError·src/error/TypeORMError.ts— base errordocs/docs/guides/1-active-record-data-mapper.md— AR vs DM patterns,findByNameQueryBuilder, "which should I choose" ·docs/docs/getting-started.md—reflect-metadata+experimentalDecoratorssetupCHANGELOG.md—0.0.1first release,1.0.0@ 2026-05-19- Shared vocabulary: SQL & ORM concepts (abstraction ladder · query models · injection · schema/migrations · ORM patterns · N+1 · effects & transactions · pools)
- Related deep-dives in this survey:
Prisma·Drizzle·Kysely·Sequelize·EF Core·Hibernate·SQLAlchemy·Django ORM·SeaORM