mardi 7 janvier 2020

Is it possible to write generics in a way that still enforces type safety?

Let's say I have multiple classes that have a similar method. In this case, a bunch of services that have a saveModel method:

public async saveModel(newModel: IModel): Promise<IModel> {
    return await newModel.save();
}

The generic method I made looks like this:

public async saveModel<P extends Document>(newModel: P): Promise<P> {
    return await newModel.save();
}

All of my services (the ones that use that method) extend a class that contains that generic method, so now on any of those services, I can call answerService.saveModel(newAnswer) and it'll save the answer. Everything was dandy until I realized that I can put an object of type IQuestion in there as well. And an object of type IDinosaur if I wanted, as long as it extends the Mongoose Document interface.

Is there any way for me to enforce certain interfaces to be used in each service? How would I make sure that the answerService only saves objects with type IAnswer? Does the answerService need to implement an interface that has that saveModel signature like this:

interface IAnswerService {
    saveModel(newModel: IAnswer): Promise<IAnswer>;
}

This is a skeleton of what the entire thing looks like:

EntityService.ts

import { Document } from 'mongoose';
export class EntityService implements IEntityService {
  public async saveModel<P extends Document>(newModel: P): Promise<P> {
    try {
      const savedModel = await newModel.save();
      return savedModel;
    } catch (err) {
      console.log(err);
      return null;
    }
  }
}

answer.service.ts

import { Answer, IAnswer } from '@models/answer.model';
import { EntityService } from '@classes/EntityService';

class AnswerService extends EntityService {
  private static instance: AnswerService;

  private constructor() {
    super();
  }

  static getInstance(): AnswerService {
    if (!AnswerService.instance) {
      AnswerService.instance = new AnswerService();
    }

    return AnswerService.instance;
  }
}

const answerService = AnswerService.getInstance();
export default answerService;

answer.model.ts

import mongoose, { Schema, Document, Model } from 'mongoose';

export interface IAnswer extends Document {
  answerText: string;
  posterID: string;
}

const AnswerSchema: Schema = new Schema({
  answerText: {type: String, required: true},
  posterID: {type: String, required: true}
}, {
  minimize: false
});

export const Answer: Model<IAnswer> = mongoose.model<IAnswer>('Answer', AnswerSchema);

All other interfaces inherit from the same things that IAnswer does (Model, Schema, Document).

And usage of the AnswerService looks generally like this:

import answerService from 'answer.service';
import { Answer } from 'answer.model';

const answer = new Answer({
    answerText: 'Stuff',
    posterID: '123456789'
})

answerService.saveModel(answer);

Aucun commentaire:

Enregistrer un commentaire