Can type safety be maintained while utilizing generics in code?

Suppose I have multiple classes with a similar method for saving models. For example, many services that include a saveModel method:

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

I created a generic method like this:

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

All the services (using this method) extend a class containing the generic method. Now, any service can call

answerService.saveModel(newAnswer)
and save the answer. However, things got complicated when I realized I could pass an object of type IQuestion or IDinosaur as long as it extends the Mongoose Document interface.

Is there a way to enforce specific interfaces for each service? How can I ensure that only objects of type IAnswer are saved by the answerService? Does the answerService need to implement an interface with the saveModel signature like this:

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

This is an overview of how everything looks:

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 elements as IAnswer (Model, Schema, Document).

The usage of the AnswerService typically looks like this:

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

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

answerService.saveModel(answer);

Answer №1

It seems like you're looking to assign a specific subtype of Document to each subclass of EntityService. In that case, rather than making saveModel() generic with P extends Document, it would be more appropriate to make the entire EntityService class generic with P. Each subclass of EntityService can then specify this generic type parameter, as shown below:

// P moved to EntityService from saveModel    
export class EntityService<P extends Document> {
  public async saveModel(newModel: P): Promise<P> {
    try {
      const savedModel = await newModel.save();
      return savedModel;
    } catch (err) {
      console.log(err);
      return null!;
    }
  }
}

// Subclass specifying the value for P   
class AnswerService extends EntityService<IAnswer> {
  private static instance: AnswerService;   
  private constructor() {
    super();
  }    
}

Now, when you have an answer of type IAnswer and a document of type Document, you will only be able to use saveModel(answer) using an instance of AnswerService:

AnswerService.getInstance().saveModel(answer); // This is correct
AnswerService.getInstance().saveModel(document); // This will result in an error since Document is not IAnswer

Hope this explanation helps. Good luck coding!

Link to Playground Code

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using LINQ with ngFor in Angular 6

Within the component.ts, I extract 15 different lookup list values and assign each one to a list, which is then bound to the corresponding <select> element in the HTML. This process functions correctly. Is there a method to streamline this code for ...

What measures can I take to guarantee a particular datatype is enforced when saving data in MongoDB?

Currently, I am utilizing mongodb for my project. I encountered an issue while saving a record with a long number - I don't want it to be saved as a float number but rather as a string. For instance, when I tried to save "171829572137423434" it was ...

There was a mistake: _v.context.$implicit.toggle cannot be used as a function

Exploring a basic recursive Treeview feature in angular4 with the code provided below. However, encountering an error when trying to expand the child view using toggle(). Encountering this exception error: ERROR TypeError: _v.context.$implicit.toggle i ...

Angular4 - Streamlined error tracking and management for all HTTP requests

I have created a customized wrapper class for handling all my http requests. The example provided only includes the get function: import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse, HttpHeaders } from &apos ...

Validation of object with incorrect child fields using Typeguard

This code snippet validates the 'Discharge' object by checking if it contains the correct children fields. interface DischargeEntry { date: string; criteria: string; } const isDischargeEntry = (discharge:unknown): discharge is DischargeEntry ...

Is there a way to retrieve the groups of match/matchAll similar to accessing an array?

My goal is to convert a version string to a number using the following code: function convertVersionToNumber(line) { const groups = line.matchAll(/^# ([0-9]).([0-9][0-9]).([0-9][0-9])\s*/g); return parseInt(groups[1] + groups[2] + groups[3]) ...

What is the correct method for storing a response in an array variable in Angular?

I am looking to save the response data from an API call in a variable and display it in the component.html file. Component.ts file : public coinsHistory = []; this.service.getCoinsHistory().subscribe( (response) => { this.handleCoinsRespon ...

"Exploiting the Power of Nullish Coalescing in Functional

The interface I am working with is defined below: interface Foo { error?: string; getError?: (param: any) => string } In this scenario, we always need to call the function getError, but it may be undefined. When dealing with base types, we can us ...

Issue TS2322: Type 'Observable<string[]>' is not compatible with type 'Observable<Sensors[]>'

This issue: error TS2322: The type 'Observable' cannot be assigned to the type 'Observable'. The type 'string[]' cannot be assigned to the type 'Sensors[]'. The type 'string' cannot be assigned to th ...

The new data is not being fetched before *ngFor is updating

In the process of developing a "Meeting List" feature that allows users to create new meetings and join existing ones. My technology stack includes: FrontEnd: Angular API: Firebase Cloud Functions DB: Firebase realtime DB To display the list of meeting ...

Issue with demonstrating the Material Angular expansion-panel feature

Exploring the world of Material Angular has been an exciting journey. Recently, I've been experimenting with adding components to my project. Currently, I'm focused on incorporating the expansion-panel component example into a dialog. However, de ...

How can I use Angular 4 typescript to deactivate a button based on the value of a boolean variable?

I am looking to define a function in TypeScript called 'isActive()', which I will then invoke on a button within my HTML file. Additionally, I have already declared a boolean variable named 'isActive'. In this scenario, I have two butto ...

Obtaining a collection of network interfaces using node.js (ioctl SIOCGIFCONF)

As a newcomer to node, I am exploring the possibilities of using node_pcap to capture packet data and perform interesting actions with it. One important aspect of this process is determining the network interface to monitor, such as "eth0". I had an idea ...

Is there a way to retrieve the ReturnType of functions based on a parameter list in Typescript?

I am struggling with defining a main function myMainFunction() that accepts a list of functions with different return types as parameters. My goal is to have the return type of myMainFunction be determined by the return types of the functions passed to it ...

Using Angular 2 to assign a function to the ngClass directive within the template

I've been searching for a solution to this issue, but so far nothing has worked for me. When I try to pass a function to the ngStyle directive, I encounter the following error: The expression 'getClass()' in ProductView has changed after i ...

Organizing an array based on designated keywords or strings

Currently, I am in the process of organizing my logframe and need to arrange my array as follows: Impact Outcomes Output Activities Here is the initial configuration of my array: { id: 15, parentId: 18, type: OUTPUT, sequence: 1 }, { id: 16, parentId: ...

Unraveling the secrets of Rxjs chaining

I have the following code that is functional and works as intended. canActivate(route: ActivatedRouteSnapshot): Observable<UrlTree | boolean> { return new Observable<UrlTree | boolean>((observer) => { this._authorizedStore .selec ...

Is it possible for an app's feature module to access routing configurations from another lazily loaded module in Angular routing?

The functionality of our App is divided into multiple feature modules that are lazily loaded. Each module is loaded under different path matches, and some modules import a shared module containing reusable components. Everything seems to be working well so ...

Incorporate the teachings of removing the nullable object key when its value is anything but 'true'

When working with Angular, I have encountered a scenario where my interface includes a nullable boolean property. However, as a developer and maintainer of the system, I know that this property only serves a purpose when it is set to 'true'. Henc ...

What is the best way to categorize an array based on a specific key, while also compiling distinct property values into a list

Suppose there is an array containing objects of type User[]: type User = { id: string; name: string; role: string; }; There may be several users in this array with the same id but different role (for example, admin or editor). The goal is to conv ...