Skip to content

How to Implement Polymorphic Relations in MikroORM 7

Problem

I was building a content management system where users can comment on posts, videos, and photos. Each content type needed its own table, but comments should work across all of them.

My first attempt looked like this:

comment.entity.ts
@Entity()
export class Comment {
@PrimaryKey()
id!: number;
@Property()
content!: string;
// Ugly: multiple nullable foreign keys
@ManyToOne(() => Post)
post?: Post;
@ManyToOne(() => Video)
video?: Video;
@ManyToOne(() => Photo)
photo?: Photo;
}

This created a sparse table with mostly null columns. Queries became messy:

comment.repository.ts
// Had to check each relationship
const comments = await em.find(Comment, {
$or: [
{ post: { id: postId } },
{ video: { id: videoId } },
{ photo: { id: photoId } }
]
});

Type safety was weak. I could accidentally set both post and video on the same comment.

Environment

  • MikroORM 7.x
  • TypeScript 5.x
  • PostgreSQL 15

The Old Workarounds

Before MikroORM 7, I tried several approaches. None were satisfactory.

Approach 1: Discriminator Strings

comment.entity.ts
@Entity()
export class Comment {
@Property()
parentType!: string; // 'post' | 'video' | 'photo'
@Property()
parentId!: number;
// No foreign key constraint
// No type safety
// Manual joins required
}

This sacrificed database integrity. No foreign keys meant no cascade deletes. I had to manually clean up orphaned comments.

Approach 2: Join Tables

comment.entity.ts
// Separate join table for each content type
@Entity()
export class PostComment {
@ManyToOne(() => Post)
post!: Post;
@ManyToOne(() => Comment)
comment!: Comment;
}
@Entity()
export class VideoComment {
@ManyToOne(() => Video)
video!: Video;
@ManyToOne(() => Comment)
comment!: Comment;
}

This exploded the number of tables. Each new content type required a new join table. Queries became complex unions.

The Solution: MikroORM 7 Polymorphic Relations

MikroORM 7 introduced native polymorphic relations. This was one of the most requested features in the ORM’s history.

Step 1: Define a Common Interface

First, I created an interface that all commentable entities implement:

commentable.interface.ts
import { Collection } from '@mikro-orm/core';
import { Comment } from './comment.entity';
export interface Commentable {
id: number;
comments: Collection<Comment>;
}

Step 2: Create Entity Types

Each content type implements the interface:

post.entity.ts
@Entity()
export class Post implements Commentable {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property()
content!: string;
@OneToMany(() => Comment, comment => comment.parent)
comments = new Collection<Comment>(this);
}
video.entity.ts
@Entity()
export class Video implements Commentable {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property()
duration!: number;
@OneToMany(() => Comment, comment => comment.parent)
comments = new Collection<Comment>(this);
}
photo.entity.ts
@Entity()
export class Photo implements Commentable {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property()
url!: string;
@OneToMany(() => Comment, comment => comment.parent)
comments = new Collection<Comment>(this);
}

Step 3: Define the Polymorphic Relation

The magic happens in the Comment entity:

comment.entity.ts
@Entity()
export class Comment {
@PrimaryKey()
id!: number;
@Property()
content!: string;
@Property()
createdAt = new Date();
// Polymorphic relation - can reference Post, Video, or Photo
@ManyToOne(() => [Post, Video, Photo])
parent!: Post | Video | Photo;
}

The @ManyToOne(() => [Post, Video, Photo]) syntax tells MikroORM that this relation can point to any of these entity types.

How It Works Under the Hood

MikroORM creates a discriminator column to track the entity type:

CREATE TABLE comment (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
parent_id INTEGER NOT NULL,
parent_type VARCHAR(255) NOT NULL -- Discriminator column
);

When I query comments, MikroORM automatically joins the correct parent table based on the discriminator.

Querying Polymorphic Relations

Find Comments by Parent

comment.service.ts
// Find all comments on a specific post
const postComments = await em.find(Comment, {
parent: { id: postId }
});
// Works for any parent type
const videoComments = await em.find(Comment, {
parent: { id: videoId }
});

Load Comments with Parent

comment.service.ts
const comments = await em.find(Comment, {}, {
populate: ['parent']
});
// Type narrowing for safety
for (const comment of comments) {
if (comment.parent instanceof Post) {
console.log(`Comment on post: ${comment.parent.title}`);
} else if (comment.parent instanceof Video) {
console.log(`Comment on video: ${comment.parent.title}`);
} else if (comment.parent instanceof Photo) {
console.log(`Comment on photo: ${comment.parent.title}`);
}
}

Query Builder Integration

comment.repository.ts
const comments = await em.createQueryBuilder(Comment, 'c')
.leftJoinAndSelect('c.parent', 'p')
.where({ 'p.title': { $like: '%important%' } })
.getResult();

Table-Per-Type Inheritance Alternative

For scenarios where entities share common fields, MikroORM 7 also supports Table-Per-Type inheritance:

media.entity.ts
@Entity({
discriminatorColumn: 'type',
discriminatorMap: {
post: 'Post',
video: 'Video',
photo: 'Photo',
},
})
export abstract class Media {
@PrimaryKey()
id!: number;
@Property()
title!: string;
@Property()
createdAt = new Date();
}
post.entity.ts
@Entity({ discriminatorValue: 'post' })
export class Post extends Media {
@Property()
content!: string;
@OneToMany(() => Comment, comment => comment.parent)
comments = new Collection<Comment>(this);
}

This approach creates a base table for shared columns and separate tables for type-specific columns.

Common Mistakes I Made

Mistake 1: Forgetting Type Narrowing

comment.service.ts
// WRONG: TypeScript doesn't know the specific type
const title = comment.parent.title; // Error if Photo has no title
// CORRECT: Narrow the type first
if (comment.parent instanceof Post) {
const title = comment.parent.title; // TypeScript knows it's a Post
}

Mistake 2: Mixing Inheritance Strategies

I initially tried mixing Single Table Inheritance with polymorphic relations. This caused confusing errors. Stick to one strategy per entity hierarchy.

Mistake 3: Over-Engineering

Not every relationship needs polymorphism. I added it to a simple User -> Profile relationship where a regular @OneToOne would have been cleaner.

When to Use Polymorphic Relations

I use polymorphic relations when:

  • Multiple entity types share the same relationship pattern (comments, tags, likes)
  • The number of entity types might grow (adding new content types)
  • I need type-safe queries across different parent types
  • Foreign key integrity matters

I avoid polymorphic relations when:

  • Only one or two entity types are involved
  • The relationship is unlikely to change
  • Simplicity outweighs flexibility

Performance Considerations

Polymorphic queries can be slower than direct relationships. MikroORM optimizes with:

  • Batch loading to avoid N+1 queries
  • Smart JOIN strategies based on discriminator
  • Eager loading support for populated relations

I always index the discriminator column:

CREATE INDEX idx_comment_parent ON comment(parent_type, parent_id);

Summary

MikroORM 7’s polymorphic relations solved a problem that had plagued my codebase for months. The old workarounds with nullable foreign keys and discriminator strings are gone.

Key takeaways:

  • Use @ManyToOne(() => [Entity1, Entity2, ...]) for polymorphic relations
  • Implement a common interface for type safety
  • Always narrow types when accessing polymorphic properties
  • Index discriminator columns for performance
  • Don’t over-engineer simple relationships

The feature was worth the wait. My comment system is now clean, type-safe, and maintainable.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments