Defining Entities via defineEntity
The defineEntity helper is the recommended way to define entities programmatically without decorators. It is built on top of EntitySchema, leveraging TypeScript's type inference to generate entity types automatically.
defineEntity
Use defineEntity to declare your entities. It returns an EntitySchema instance with full type information.
import { defineEntity, p } from '@mikro-orm/core';
const BookSchema = defineEntity({
name: 'Book',
properties: {
id: p.integer().primary(),
title: p.string(),
author: () => p.manyToOne(Author).inversedBy('books'),
tags: () => p.manyToMany(BookTag).inversedBy('books').fixedOrder(),
},
});
export class Book extends BookSchema.class {}
BookSchema.setClass(Book);
The p shortcut is also available as defineEntity.properties.
The defineEntity + class pattern (recommended)
When you extend the auto-generated class and register it via setClass(), you get several benefits:
- Clean hover types: Hovering over a
Bookvariable showsBook— not a complex intersection type with generics and symbols - Better performance: A real named class is more efficient than the dynamically generated anonymous class used by the pure approach
- Custom methods: Add domain logic directly on entity instances without any workarounds
- No property duplication: Properties are defined once in the schema and automatically inherited by the class
const AuthorSchema = defineEntity({
name: 'Author',
properties: {
id: p.integer().primary(),
firstName: p.string(),
lastName: p.string(),
books: () => p.oneToMany(Book).mappedBy('author'),
},
});
class Author extends AuthorSchema.class {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
AuthorSchema.setClass(Author);
// Usage:
const author = em.create(Author, { firstName: 'John', lastName: 'Doe' });
console.log(author.fullName()); // "John Doe"
Important:
setClass()must be called before the ORM discovery process runs (i.e., beforeMikroORM.init()). Make sure to call it at module load time, right after defining the extended class.
Pure defineEntity (without a class)
You can also use defineEntity without extending a class. This is more compact for simple entities but produces complex computed types on hover.
import { type InferEntity, defineEntity, p } from '@mikro-orm/core';
export const Book = defineEntity({
name: 'Book',
properties: {
id: p.integer().primary(),
title: p.string(),
author: () => p.manyToOne(Author).inversedBy('books'),
tags: () => p.manyToMany(BookTag).inversedBy('books').fixedOrder(),
},
});
// Use InferEntity to extract the entity type
export type IBook = InferEntity<typeof Book>;
When creating new entity instances without a class, use em.create() which will create an instance of the internally generated class:
const book = em.create(Book, { title: 'My Book', author });
await em.flush();
Reusing base properties
There are two ways to share common properties across entities with defineEntity:
Via composition (shared property objects)
The simplest approach — spread a shared property object into each entity's properties:
const p = defineEntity.properties;
const baseProperties = {
id: p.integer().primary(),
createdAt: p.datetime().onCreate(() => new Date()),
updatedAt: p.datetime()
.onCreate(() => new Date())
.onUpdate(() => new Date()),
};
const BookSchema = defineEntity({
name: 'Book',
properties: {
...baseProperties,
title: p.string(),
author: () => p.manyToOne(Author),
},
});
export class Book extends BookSchema.class {}
BookSchema.setClass(Book);
Via extends with property initializers
When using the defineEntity + class pattern with extends, the auto-generated class inherits from the parent class at the JavaScript level. This means property initializers defined on the base class (like id = v4() or createdAt = new Date()) run automatically when constructing child entities via new:
const BaseSchema = defineEntity({
name: 'BaseEntity',
abstract: true,
properties: {
id: p.string().primary(),
createdAt: p.datetime(),
updatedAt: p.datetime(),
},
});
export class Base extends BaseSchema.class {
id = v4();
createdAt = new Date();
updatedAt = new Date();
}
BaseSchema.setClass(Base);
const UserSchema = defineEntity({
name: 'User',
extends: BaseSchema,
properties: {
email: p.string().unique(),
name: p.string(),
},
});
export class User extends UserSchema.class {
name = '';
}
UserSchema.setClass(User);
// id, createdAt, updatedAt are initialized from Base's property initializers
const user = new User();
console.log(user.id); // a UUID string
console.log(user.createdAt); // current Date
This approach is useful when you want constructor-level defaults that work without an
EntityManagercontext (i.e., with plainnew). If you only need defaults during persistence, you can useonCreatehooks instead — see Mapped Superclasses for more details.
Inheriting base class methods
Methods declared on the base class are always inherited at runtime — the auto-generated child class extends the base class, so a child instance is instanceof the base and can call its methods. At the type level, however, extends: BaseSchema only carries the mapped properties; TypeScript cannot see methods you attach to the schema later via setClass. To make those methods visible on the child entity type, point extends at the base class instead of the schema:
export class Base extends BaseSchema.class {
id = v4();
createdAt = new Date();
updatedAt = new Date();
wasUpdated(): boolean {
return this.updatedAt > this.createdAt;
}
}
BaseSchema.setClass(Base);
const UserSchema = defineEntity({
name: 'User',
extends: Base, // the class, not BaseSchema — exposes `wasUpdated()` on the child type
properties: {
email: p.string().unique(),
},
});
export class User extends UserSchema.class {
describe() {
return this.wasUpdated() ? `${this.email} (edited)` : this.email;
}
}
UserSchema.setClass(User);
Both forms behave identically at runtime; passing the class simply lets TypeScript propagate the inherited method signatures. Extending the schema (extends: BaseSchema) still inherits all mapped properties and is the right choice when the base only contributes columns.
Property types
defineEntity.properties (aliased as p) provides all MikroORM built-in types. To use custom types, use p.type():
const properties = {
string: p.string(),
float: p.float(),
boolean: p.boolean(),
json: p.json<{ foo: string; bar: number }>().nullable(),
stringArray: p.type(ArrayType<string>).nullable(),
numericArray: p.type(new ArrayType(i => +i)).nullable(),
point: p.type(PointType).nullable(),
};
Relation modifiers: .ref() and .lazyRef()
For m:1 / 1:1 relations, you can opt into compile-time populate-state safety:
.ref()wraps the runtime value in aReference, exposing.$/.get()/.load()— seeRef<T>..lazyRef()is a type-only marker — the runtime stays a plain entity (no wrapper), but TypeScript hides non-PK access untilLoaded<>narrows it. SeeLazyRef<T>.
const BookSchema = defineEntity({
name: 'Book',
properties: {
id: p.integer().primary(),
author: () => p.manyToOne(AuthorSchema).ref(), // `Ref<Author>`
publisher: () => p.manyToOne(PublisherSchema).lazyRef(), // `LazyRef<Publisher>`
},
});
MongoDB example
- defineEntity + class
- defineEntity
const BookTagSchema = defineEntity({
name: 'BookTag',
properties: {
_id: p.type(ObjectId).primary(),
id: p.string().serializedPrimaryKey(),
name: p.string(),
books: () => p.manyToMany(Book).mappedBy('tags'),
},
});
export class BookTag extends BookTagSchema.class {}
BookTagSchema.setClass(BookTag);
export const BookTag = defineEntity({
name: 'BookTag',
properties: {
_id: p.type(ObjectId).primary(),
id: p.string().serializedPrimaryKey(),
name: p.string(),
books: () => p.manyToMany(Book).mappedBy('tags'),
},
});
export type IBookTag = InferEntity<typeof BookTag>;
Hooks example
Hooks can be registered in two ways:
hooksproperty — pass an object with arrays of hook handlers directly in thedefineEntitycall.addHookmethod — register hook handlers after the entity is defined.
Both approaches accept functions (arrow functions, named functions, async functions). The entity instance is available via args.entity. See Events and Lifecycle Hooks for the full list of available hooks and EventArgs details.
- defineEntity + class
- defineEntity
Use addHook after the class is defined for full type safety:
const BookTagSchema = defineEntity({
name: 'BookTag',
properties: {
_id: p.type(ObjectId).primary(),
id: p.string().serializedPrimaryKey(),
name: p.string(),
version: p.integer(),
books: () => p.manyToMany(Book).mappedBy('tags'),
},
});
export class BookTag extends BookTagSchema.class {}
BookTagSchema.setClass(BookTag);
BookTagSchema.addHook('beforeCreate', (args: EventArgs<BookTag>) => {
args.entity.version = 1;
});
BookTagSchema.addHook('beforeUpdate', (args: EventArgs<BookTag>) => {
args.entity.version++;
});
You can also pass hooks inline via the
hooksproperty, butargs.entitywill be typed asanythere because the entity type is not yet known. Explicitly typing the parameter (e.g.EventArgs<BookTag>) won't work either, as it would create a circular reference. UseaddHookafter the class is defined to get full type safety.
Use addHook after the entity is defined for full type safety:
export const BookTag = defineEntity({
name: 'BookTag',
properties: {
_id: p.type(ObjectId).primary(),
id: p.string().serializedPrimaryKey(),
name: p.string(),
version: p.integer(),
books: () => p.manyToMany(Book).mappedBy('tags'),
},
});
export type IBookTag = InferEntity<typeof BookTag>;
BookTag.addHook('beforeCreate', (args: EventArgs<IBookTag>) => {
args.entity.version = 1;
});
BookTag.addHook('beforeUpdate', (args: EventArgs<IBookTag>) => {
args.entity.version++;
});
You can also pass hooks inline via the
hooksproperty, butargs.entitywill be typed asanythere because the entity type is not yet known. Explicitly typing the parameter (e.g.EventArgs<IBookTag>) won't work either, as it would create a circular reference. UseaddHookafter the entity and its type alias are defined to get full type safety.
EntitySchema (low-level API)
defineEntity returns an EntitySchema instance — the same class you can instantiate directly. Using EntitySchema directly is generally not necessary, as defineEntity provides a more ergonomic API with full type inference. However, it's available for advanced use cases or vanilla JavaScript projects.
export interface IBook {
title: string;
author: Author;
publisher: Publisher;
tags: Collection<BookTag>;
}
export const BookSchema = new EntitySchema<IBook>({
name: 'Book',
extends: CustomBaseEntitySchema,
properties: {
title: { type: 'string' },
author: { kind: 'm:1', entity: () => Author, inversedBy: 'books' },
publisher: { kind: 'm:1', entity: () => Publisher, inversedBy: 'books' },
tags: { kind: 'm:n', entity: () => BookTag, inversedBy: 'books', fixedOrder: true },
},
});
Using a class with EntitySchema
You can pass a class option instead of name:
export class Author extends CustomBaseEntity {
name: string;
email: string;
constructor(name: string, email: string) {
super();
this.name = name;
this.email = email;
}
}
export const AuthorSchema = new EntitySchema({
class: Author,
extends: CustomBaseEntitySchema,
properties: {
name: { type: 'string' },
email: { type: 'string', unique: true },
},
});
Configuration Reference
The parameter of EntitySchema requires either name or class. When using class, extends will be automatically inferred. Additional parameters:
name: string;
class: Constructor<T>;
extends: string;
tableName: string; // alias for `collection: string`
properties: { [K in keyof T & string]: EntityProperty<T[K]> };
indexes: { properties: string | string[]; name?: string; type?: string }[];
uniques: { properties: string | string[]; name?: string }[];
repository: () => Constructor<EntityRepository<T>>;
hooks: Partial<Record<keyof typeof EventType, ((string & keyof T) | NonNullable<EventSubscriber[keyof EventSubscriber]>)[]>>;
abstract: boolean;
orderBy: QueryOrderMap<T> | QueryOrderMap<T>[]; // default ordering for the entity
As a value for
typeyou can also use one ofString/Number/Boolean/Date.