How to implement TypeORM entity inheritance? Including single table, class table, and concrete table inheritance
Entity inheritance is an important feature of object-oriented programming. TypeORM provides multiple inheritance patterns, allowing developers to better organize and manage entity classes.
Inheritance Pattern Overview
TypeORM supports three main inheritance patterns:
- Single Table Inheritance
- Class Table Inheritance
- Concrete Table Inheritance
Single Table Inheritance
Basic Concept
Single table inheritance stores all subclass data in the same table, using a discriminator column to distinguish different subclasses.
Implementation Example
typescriptimport { Entity, PrimaryGeneratedColumn, Column, ChildEntity, DiscriminatorColumn, DiscriminatorValue } from 'typeorm'; @Entity() @DiscriminatorColumn({ name: 'type' }) export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column({ type: 'text' }) body: string; @Column({ type: 'timestamp' }) createdAt: Date; @Column({ type: 'timestamp' }) updatedAt: Date; } @ChildEntity() @DiscriminatorValue('article') export class Article extends Content { @Column() author: string; @Column() category: string; } @ChildEntity() @DiscriminatorValue('video') export class Video extends Content { @Column() url: string; @Column() duration: number; } @ChildEntity() @DiscriminatorValue('podcast') export class Podcast extends Content { @Column() audioUrl: string; @Column() episodeNumber: number; }
Usage Example
typescript// Query all content const allContent = await dataSource.getRepository(Content).find(); // Query specific type of content const articles = await dataSource.getRepository(Article).find(); const videos = await dataSource.getRepository(Video).find(); // Create different types of content const article = new Article(); article.title = 'TypeORM Tutorial'; article.body = 'Content...'; article.author = 'John Doe'; article.category = 'Programming'; const video = new Video(); video.title = 'TypeORM Video Guide'; video.body = 'Video content...'; video.url = 'https://example.com/video'; video.duration = 600; await dataSource.getRepository(Content).save([article, video]);
Pros and Cons
Pros:
- Good query performance, all data in one table
- Simple relationship management
- Suitable for cases with small field differences between subclasses
Cons:
- Table may have many NULL columns
- Not suitable for cases with large field differences between subclasses
- Adding new subclasses requires modifying table structure
Class Table Inheritance
Basic Concept
Class table inheritance creates separate tables for each class, with subclass tables linked to parent class table through foreign keys.
Implementation Example
typescriptimport { Entity, PrimaryGeneratedColumn, Column, ChildEntity, TableInheritance } from 'typeorm'; @Entity() @TableInheritance({ column: { type: 'varchar', name: 'type' } }) export class Person { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() email: string; @Column({ type: 'date' }) birthDate: Date; } @ChildEntity() export class Employee extends Person { @Column() position: string; @Column() department: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) salary: number; } @ChildEntity() export class Customer extends Person { @Column() companyName: string; @Column() industry: string; @Column({ type: 'int' }) loyaltyPoints: number; }
Usage Example
typescript// Query all people const allPeople = await dataSource.getRepository(Person).find(); // Query specific type of people const employees = await dataSource.getRepository(Employee).find(); const customers = await dataSource.getRepository(Customer).find(); // Create different types of people const employee = new Employee(); employee.name = 'John Doe'; employee.email = 'john@example.com'; employee.birthDate = new Date('1990-01-01'); employee.position = 'Developer'; employee.department = 'IT'; employee.salary = 75000; const customer = new Customer(); customer.name = 'Jane Smith'; customer.email = 'jane@example.com'; customer.birthDate = new Date('1985-05-15'); customer.companyName = 'Acme Corp'; customer.industry = 'Technology'; customer.loyaltyPoints = 1000; await dataSource.getRepository(Person).save([employee, customer]);
Pros and Cons
Pros:
- Data normalization, avoids NULL columns
- Each subclass has its own table
- Suitable for cases with large field differences between subclasses
Cons:
- Queries require JOIN, performance may be poor
- Complex relationship management
- Adding new subclasses requires creating new tables
Concrete Table Inheritance
Basic Concept
Concrete table inheritance creates separate tables for each concrete subclass, without creating a parent class table.
Implementation Example
typescriptimport { Entity, PrimaryGeneratedColumn, Column, AbstractEntity } from 'typeorm'; @AbstractEntity() export abstract class Vehicle { @PrimaryGeneratedColumn() id: number; @Column() brand: string; @Column() model: string; @Column({ type: 'int' }) year: number; @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number; } @Entity() export class Car extends Vehicle { @Column() fuelType: string; @Column({ type: 'int' }) numberOfDoors: number; @Column({ type: 'boolean' }) isConvertible: boolean; } @Entity() export class Motorcycle extends Vehicle { @Column() engineType: string; @Column({ type: 'int' }) displacement: number; @Column({ type: 'boolean' }) hasSidecar: boolean; }
Usage Example
typescript// Query specific type of vehicle const cars = await dataSource.getRepository(Car).find(); const motorcycles = await dataSource.getRepository(Motorcycle).find(); // Create different types of vehicles const car = new Car(); car.brand = 'Toyota'; car.model = 'Camry'; car.year = 2024; car.price = 35000; car.fuelType = 'Hybrid'; car.numberOfDoors = 4; car.isConvertible = false; const motorcycle = new Motorcycle(); motorcycle.brand = 'Harley-Davidson'; motorcycle.model = 'Street 750'; motorcycle.year = 2024; motorcycle.price = 8000; motorcycle.engineType = 'V-Twin'; motorcycle.displacement = 750; motorcycle.hasSidecar = false; await Promise.all([ dataSource.getRepository(Car).save(car), dataSource.getRepository(Motorcycle).save(motorcycle) ]);
Pros and Cons
Pros:
- Each table is complete, no NULL columns
- Good query performance
- Suitable for completely independent subclasses
Cons:
- Parent class fields repeated in all subclass tables
- Difficult to query all subclasses
- Not suitable for cases requiring unified parent class queries
Inheritance and Relationships
Using Relationships in Inheritance
typescript@Entity() @DiscriminatorColumn({ name: 'type' }) export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @OneToMany(() => Comment, comment => comment.content) comments: Comment[]; } @ChildEntity() @DiscriminatorValue('article') export class Article extends Content { @Column() author: string; @Column() category: string; } @Entity() export class Comment { @PrimaryGeneratedColumn() id: number; @Column() text: string; @ManyToOne(() => Content, content => content.comments) content: Content; } // Usage example const article = await dataSource.getRepository(Article).findOne({ where: { id: 1 }, relations: ['comments'] });
Polymorphic Relationships
typescript@Entity() export class Tag { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column() entityType: string; // 'article' or 'video' @Column() entityId: number; } @Entity() @DiscriminatorColumn({ name: 'type' }) export class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @OneToMany(() => Tag, tag => tag.entityId = this.id && tag.entityType = 'article' ) tags: Tag[]; } @ChildEntity() @DiscriminatorValue('article') export class Article extends Content { @Column() author: string; } @ChildEntity() @DiscriminatorValue('video') export class Video extends Content { @Column() url: string; }
Inheritance and Queries
Querying All Subclasses
typescript// Single table inheritance: query all content const allContent = await dataSource.getRepository(Content).find(); // Class table inheritance: query all people const allPeople = await dataSource.getRepository(Person).find({ relations: true // Load subclass data }); // Concrete table inheritance: need to query separately const cars = await dataSource.getRepository(Car).find(); const motorcycles = await dataSource.getRepository(Motorcycle).find();
Filtering by Type
typescript// Filter using discriminator value const articles = await dataSource.getRepository(Content).find({ where: { type: 'article' as any } }); // Or use subclass Repository const articles = await dataSource.getRepository(Article).find();
Complex Queries
typescript// Query specific type with related data const articlesWithComments = await dataSource .getRepository(Article) .createQueryBuilder('article') .leftJoinAndSelect('article.comments', 'comment') .where('article.category = :category', { category: 'Technology' }) .getMany(); // Use QueryBuilder to query by type const contents = await dataSource .getRepository(Content) .createQueryBuilder('content') .where('content.type IN (:...types)', { types: ['article', 'video'] }) .getMany();
Inheritance and Migrations
Single Table Inheritance Migration
typescriptimport { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class CreateContentTable1234567890123 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { await queryRunner.createTable( new Table({ name: 'content', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'title', type: 'varchar', }, { name: 'body', type: 'text', }, { name: 'type', // Discriminator column type: 'varchar', default: "'article'", }, { name: 'author', // Article specific field type: 'varchar', isNullable: true, }, { name: 'category', // Article specific field type: 'varchar', isNullable: true, }, { name: 'url', // Video specific field type: 'varchar', isNullable: true, }, { name: 'duration', // Video specific field type: 'int', isNullable: true, }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('content'); } }
Class Table Inheritance Migration
typescriptexport class CreatePersonTables1234567890124 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise<void> { // Create parent class table await queryRunner.createTable( new Table({ name: 'person', columns: [ { name: 'id', type: 'int', isPrimary: true, isGenerated: true, generationStrategy: 'increment', }, { name: 'name', type: 'varchar', }, { name: 'email', type: 'varchar', }, { name: 'birthDate', type: 'date', }, { name: 'type', // Discriminator column type: 'varchar', }, ], }), true ); // Create subclass table await queryRunner.createTable( new Table({ name: 'employee', columns: [ { name: 'id', type: 'int', isPrimary: true, }, { name: 'position', type: 'varchar', }, { name: 'department', type: 'varchar', }, { name: 'salary', type: 'decimal', precision: 10, scale: 2, }, ], foreignKeys: [ { columnNames: ['id'], referencedColumnNames: ['id'], referencedTableName: 'person', onDelete: 'CASCADE', }, ], }), true ); } public async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.dropTable('employee'); await queryRunner.dropTable('person'); } }
Best Practices
Choosing the Right Inheritance Pattern
typescript// ✅ Single table inheritance: small field differences between subclasses @Entity() @DiscriminatorColumn({ name: 'type' }) export class User { @PrimaryGeneratedColumn() id: number; @Column() username: string; @Column() email: string; } @ChildEntity() @DiscriminatorValue('admin') export class Admin extends User { @Column() permissions: string; } // ✅ Class table inheritance: large field differences between subclasses @Entity() @TableInheritance({ column: { type: 'varchar', name: 'type' } }) export class Product { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ type: 'decimal', precision: 10, scale: 2 }) price: number; } @ChildEntity() export class PhysicalProduct extends Product { @Column() weight: number; @Column() dimensions: string; } @ChildEntity() export class DigitalProduct extends Product { @Column() downloadUrl: string; @Column() fileSize: number; } // ✅ Concrete table inheritance: completely independent subclasses @AbstractEntity() export abstract class Notification { @PrimaryGeneratedColumn() id: number; @Column() message: string; @Column({ type: 'timestamp' }) createdAt: Date; } @Entity() export class EmailNotification extends Notification { @Column() recipientEmail: string; @Column() subject: string; } @Entity() export class SMSNotification extends Notification { @Column() phoneNumber: string; @Column() sender: string; }
Avoid Deep Inheritance
typescript// ❌ Bad: deep inheritance @Entity() @DiscriminatorColumn({ name: 'type' }) export class Animal { @PrimaryGeneratedColumn() id: number; @Column() name: string; } @ChildEntity() @DiscriminatorValue('mammal') export class Mammal extends Animal { @Column() furColor: string; } @ChildEntity() @DiscriminatorValue('dog') export class Dog extends Mammal { @Column() breed: string; } // ✅ Good: flat inheritance structure @Entity() @DiscriminatorColumn({ name: 'type' }) export class Animal { @PrimaryGeneratedColumn() id: number; @Column() name: string; @Column({ nullable: true }) furColor: string; @Column({ nullable: true }) breed: string; } @ChildEntity() @DiscriminatorValue('dog') export class Dog extends Animal { // Dog specific properties } @ChildEntity() @DiscriminatorValue('cat') export class Cat extends Animal { // Cat specific properties }
TypeORM's entity inheritance feature provides powerful object-oriented programming support. Proper use of inheritance can improve code maintainability and extensibility.