How to effectively utilize discriminator types in a NestJS module?

  Kiến thức lập trình

I’m currently developing a NestJS module aimed at storing notifications of various kinds within the same table by utilizing discriminators. Despite eliminating the use of TypeScript’s as keyword to keep things straightforward and rely on type inference, I’m encountering difficulties in achieving the desired type narrowing and inference with Mongoose’s functionality.

Although Mongoose’s functionality seems well-designed for typing, I’m facing challenges in making type inference work as expected. I’m hoping to receive some guidance on how to effectively manage discriminator types in my code.

I’ll provide the content of the file along with links to StackBlitz and GitHub – notification.service.ts for reference. Any insights or solutions would be greatly appreciated!

import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { HydratedDocument, Model, Types } from 'mongoose';
import {
  ISampleAssignedNotification,
  ITowLogChangeNotification,
  TNotification,
} from './notification.type';
import { Notification } from './schema/notification.schema';

@Injectable()
export class NotificationService {
  constructor(
    @InjectModel(Notification.name)
    private notificationModel: Model<TNotification>,
    @InjectModel('TowLogChange')
    private towLogChangeModel: Model<ITowLogChangeNotification>,
    @InjectModel('SampleAssigned')
    private sampleAssignedModel: Model<ISampleAssignedNotification>,
  ) {}

  async testA() {
    const doc = await this.createA({
      userId: new Types.ObjectId(),
      type: 'TowLogChange',
      metadata: {
        towId: new Types.ObjectId(),
        newStatus: 'some status',
      },
    } as ITowLogChangeNotification);
    console.log(doc.metadata.towId);
  }

  async createA<T extends TNotification>(notification: T) {
    return this.notificationModel.create(notification) as Promise<
      HydratedDocument<T>
    >;
  }

  async testB() {
    const doc = await this.createB({
      userId: new Types.ObjectId(),
      type: 'TowLogChange',
      metadata: {
        towId: new Types.ObjectId(),
        newStatus: 'some status',
      },
    } as ITowLogChangeNotification);

    // towId can not be found.
    // console.log(doc.metadata.towId);

    // Too complicated.
    // Type is not inferred as in variant A
    console.log(
      (doc as HydratedDocument<ITowLogChangeNotification>).metadata.towId,
    );
  }

  async createB<T extends TNotification>(notification: T) {
    if (notification.type == 'TowLogChange') {
      return this.towLogChangeModel.create(notification);
    } else if (notification.type == 'SampleAssigned') {
      return this.sampleAssignedModel.create(notification);
    }

    throw new Error('Unpredictable logic place.');
  }
}

This is a simplified version of the problem

type TLetterA = 'a';
type TLetterB = 'b';

type TLetters = TLetterA | TLetterB;

const letterA: TLetterA = 'a';
const letterB: TLetterB = 'b';
const letterAorB: TLetters = letterA;

function passLetter<T extends TLetters>(letter: T): T {
    if (letter == 'a') {
        return letter
    } else if (letter == 'b') {
        return letter;
    }

    throw new Error('Some error....');
}

const passLetterA = passLetter(letterA);
//   ^?

Feel free to let me know if you’d like further adjustments!

P.S. Yes it was edited by ChatGPT. I’ve been looking for solution for few days with it.

LEAVE A COMMENT