Search code examples
javascripttypescriptnestjsdto

How to Ensure Type Safety in TypeScript When Transforming Objects Through Multiple Stages?


I'm working on a NestJS application where I need to create different types of entities (like Users, Products, Orders, etc.) and apply some transformations before saving them to the database. Specifically, I want to add a version number and some IDs to the entity if it's of a certain type (e.g., User).

I created new interfaces (EntityWithVersion, EntityWithIds) to handle the additional properties. Is this the best approach to ensure type safety, or is it better to use an interface that represents the whole object?

I will be using NestJS, since I'm familiar with it, to illustrate this example:

interface CreateEntityDto {
  type: EntityType;
  data: any; // Generalized to any type
}

enum EntityType {
  User,
  Product,
  Order,
}

interface EntityWithVersion extends CreateEntityDto {
  version: number;
}

interface EntityWithIds extends EntityWithVersion {
  ids: string[];
}

@Injectable()
export class EntityService {
  constructor(
    @InjectModel('Entity') private readonly entityModel: Model<Entity>,
  ) {}

  async create(createEntityDto: CreateEntityDto): Promise<Entity> {
    if (createEntityDto.type === EntityType.User && createEntityDto.data) {
      const entityWithVersion = await this.addEntityVersion(createEntityDto);
      const entityWithIds = await this.addEntityIds(entityWithVersion);
      return await this.entityModel.create(entityWithIds);
    }
    return await this.entityModel.create(createEntityDto);
  }

  async addEntityVersion(
    entity: CreateEntityDto,
  ): Promise<EntityWithVersion> {
    return {
      ...entity,
      version: 1, 
    };
  }

  async addEntityIds(
    entity: EntityWithVersion,
  ): Promise<EntityWithIds> {
    return {
      ...entity,
      ids: ['id1', 'id2'],
    };
  }
}

In my project, I needed to manage objects that go through various transformations, with new properties being added at different stages. To ensure type safety, I decided to create new interfaces that extend the original data transfer object (DTO) each time a new property was added.


Solution

  • Your approach to declaring specific interfaces is well-suited for type safety and code clarity. By defining separate interfaces for each stage of transformation, you ensure that each step adds the necessary properties explicitly, reducing the chance of errors and making the code easier to understand and maintain.

    However, as the number of interfaces grows, keeping track of them all might become challenging. This could make the codebase harder to manage, especially if the transformations become more complex or the number of entities increases.

    On the other hand, the alternative approach of combining everything into one interface simplifies the code by reducing the number of interfaces. But this simplicity comes at the cost of reduced type safety since you would need to make some properties optional. This could potentially lead to errors if the required properties are not added at the right time.