Tips for creating a type-safe union typed save method

My goal is to build a versatile http service that includes a method like save(value), which in turn triggers either create(value) or update(value). What sets this requirement apart is the optional configuration feature where the type of value accepted by create can be completely different from that of update.

Illustrated below:

class HttpService {
  create(model) {
    ...
  }
  update(model) {
    ... 
  }
  save(model) {
    if(model.id === null || model.id === undefined) {
      return this.create(model);
    }
    return this.update(model);
  }
}

The generic structure resembles the following:

class HttpService<I, E, C> {
  create(model: E | C) : E;
  update(model: E) : E;
  save(model: E | C): E;
}

This amalgamation aims at addressing two scenarios:

  1. Create and update adhere to the same framework with the entity (referred to as E) possessing an id property named Id. The id itself is generic (I) and has a type of I | null | undefined. It extends MaybeEntity<I> due to this characteristic.
  2. For create and update functions, they accept distinct interfaces. Create deals with interface C extends NotEnity where there's no mention of Id - it could be absent, null, or
    undefined</code, returning <code>E
    . On the other hand, update solely accepts E extends Entity<I> where the Id must align with I.

Despite my efforts to implement these types, I encountered some difficulties.

export type Id = 'id';
export type Entity<I> = Record<Id, I>;
export type NotEntity = Partial<Record<Id, null>>;
export type MaybeEntity<I> = Partial<Record<Id, I | null >>;

class HttpService<I, E extends MaybeEntity<I>, C extends NotEntity = (E & NotEntity )> {
  create(model: C | E & NotEntity) { }

  update(model: E & Entity<I>) {}

  save(model: C | E) {
    return model.id === undefined && model.id === null
      ? this.create(model)
      : this.update(model);
  }
}

Answer №1

UPDATE! Apologies if I misunderstood your issue. I devised an interface (which could also be considered a type) for the Entity. Additionally, I crafted a type for NotEntity, essentially representing an entity without an id. Within the class, a check is performed in the save function to determine the type of id; if it is either undefined or its value is null, it signifies a not-entity and triggers the save operation. On the other hand, if we encounter an id, the update process is initiated. While there may be more efficient methods available, this approach can serve as a starting point for your own implementation.

export interface Entity<I> {
  id?: I,
  [key: string]: any
}

export type NotEntity<I> = Omit<Entity<I>, 'id'>;

export class HttpService<I, E extends Entity<I>> {
  create(model: NotEntity<I>) {}

  update(model: Entity<I>) {}

  save(model: E) {
    return typeof model.id ===  undefined || model.id === null
      ? this.create(model)
      : this.update(model);
  }
}

Answer №2

Your code is lacking a proper understanding of the ternary statement in the `save` function, as it fails to consider the limitations it imposes and does not refine the type of the `model` in both the `true` and `false` branches.

While Typescript can comprehend simple property check conditions, combining them with `&&` causes it to cease trying. It is vital to extract this multiple condition check into its own type guard function and explicitly define what it means for a condition to be `true` or `false`.

const hasId = <E extends MaybeEntity<any>>( model: E ): model is E & {id: NonNullable<E['id']>} => {
  return 'id' in model && model.id === undefined && model.id === null;
}

Currently, you are permitting `C` to demand arbitrary properties necessary for creation but may not exist in the created object. Do you truly require this functionality? While technically feasible, it would involve more advanced validation processes.

Furthermore, you're asserting that `create` cannot be invoked if an `id` already exists. Is this a strict requirement, or can the `id` simply be disregarded (e.g., when cloning an existing object with a new id)?

If neither of these restrictions are absolute necessities, the following revised code can be implemented:

export class HttpService<I, E extends MaybeEntity<I>> {
  create(model: E) { }

  update(model: E & Entity<I>) { }

  save(model: E) {
    return hasId(model)
      ? this.update(model)
      : this.create(model);
  }
}

Typescript Playground Link

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

Enhanced functionality for Thingworx using ThreeJS

I'm currently facing an issue while developing a 3 JS extension for Thingworx, specifically with the renderHtml function when working with a 3 JS canvas (Check out the code). //runtime.ts file renderHtml(): string { let htmlString = '<div ...

React/Typescript - Managing various event types within a unified onChange function

In my current project, I am working with a form that includes various types of input fields using the mui library. My goal is to gather the values from these diverse input components and store them in a single state within a Grandparent component. While I ...

Ways to get into the Directive class

@Directive({ selector: '[myHighlight]' }) export class HighlightDirective { static test: number = 5; constructor(private el: ElementRef) { } highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; } } In re ...

Error: In Typescript, it is not possible to assign the type 'false' to type 'true'

Currently, I am exploring Angular 2 and encountered a situation where I set the variable isLoading to true initially, and then switch it to false after fetching required data. However, upon executing this process, I encountered the following error message: ...

Error in AWS Cloud Development Kit: Cannot access properties of undefined while trying to read 'Parameters'

I am currently utilizing aws cdk 2.132.1 to implement a basic Lambda application. Within my project, there is one stack named AllStack.ts which acts as the parent stack for all other stacks (DynamoDB, SNS, SQS, StepFunction, etc.), here is an overview: im ...

Exploring Angular2's DOMContentLoaded Event and Lifecycle Hook

Currently, I am utilizing Angular 2 (TS) and in need of some assistance: constructor(public element:ElementRef){} ngOnInit(){ this.DOMready() } DOMready() { if (this.element) { let testPosition = this.elemen ...

When utilizing <number | null> or <number | undefined> within computed() or signals(), it may not function properly if the value is 0

I've encountered an issue while implementing signals and computed in my new Angular project. There's a computed value that holds an id, which is initially not set and will be assigned by user interaction. To handle this initial state, I attempte ...

Exploring Parquet Files with Node.js

Looking for a solution to read parquet files using NodeJS. Anyone have any suggestions? I attempted to use node-parquet but found it difficult to install and it struggled with reading numerical data types. I also explored parquetjs, however, it can only ...

Error message: "ReferenceError occurred while trying to access the Data Service in

As I embark on the journey of creating my very first MEAN stack application - an online cookbook, I have encountered a challenge in Angular. It seems like there is an issue between the service responsible for fetching recipe data from the API (RecipeDataSe ...

Issue: [ts] Unable to locate the term 'React'

Due to specific requirements, I have made some customizations to the Ionic component: range. I changed the class name of the component from Range to CustomRange (with selector: custom-range): https://github.com/ionic-team/ionic/blob/master/core/src/compon ...

How can we automate the process of assigning the hash(#) in Angular?

Is it possible to automatically assign a unique hash(#) to elements inside an ngFor loop? <div *ngFor="let item of itemsArray; index as i"> <h3 #[item][i]> {{ item }} </h3> </div> I would like the outp ...

Running ngAfterViewInit() code in Angular should be done only after Inputs() have been initialized

Within a particular component, I have implemented some code in ngAfterViewInit: @Input public stringArray: string[]; public newArray: string[]; ngAfterViewInit() { this.newArray = this.stringArray.filter(x => x.includes('a')); } I placed ...

The issue with Rxjs forkJoin not being triggered within an Angular Route Guard

I developed a user permission service that retrieves permissions from the server for a specific user. Additionally, I constructed a route guard that utilizes this service to validate whether the user possesses all the permissions specified in the route. To ...

What is the process for initiating an Angular 2 Materialize component?

I'm new to using angular2 materialize and I've found that the CSS components work perfectly fine. However, I'm facing an issue when it comes to initializing components like 'select'. I'm unsure of how or where to do this initi ...

MaterialUI Divider is designed to dynamically adjust based on the screen size. It appears horizontally on small screens and vertically on

I am currently using a MaterialUI divider that is set to be vertical on md and large screens. However, I want it to switch to being horizontal on xs and sm screens: <Divider orientation="vertical" variant="middle" flexItem /> I h ...

Error encountered with default theme styling in Material-UI React TypeScript components

Currently, I am working on integrating Material-UI (v4.4.3) with a React (v16.9.2) TypeScript (v3.6.3) website. Following the example of the AppBar component from https://material-ui.com/components/app-bar/ and the TypeScript Guide https://material-ui.com/ ...

Contrast in output between for and for..of loops demonstrated in an example

Here are two code snippets, one using a traditional for loop and the other using a for...of loop. export function reverseWordsWithTraditionalForLoop(words: string): string { const singleWords: string[] = words.split(' '); for (let i = 0; i &l ...

Combining declarations with a personalized interface using TypeScript

My goal is to enhance the express.Request type by adding a user property so that req.user will be of my custom IUser type. After learning about declaration merging, I decided to create a custom.d.ts file. declare namespace Express { export interface ...

Angular 8 throwing an ExpressionChangedAfterItHasBeenCheckedError when a method is called within the ngOnInit function

I encountered a common issue with an Angular template. I have a standard template for all my pages, containing a *ngIf directive with a spinner and another one with the router-outlet. The behavior and visibility of the spinner are controlled by an interce ...

Is there a way for me to showcase a particular PDF file from an S3 bucket using a custom URL that corresponds to the object's name

Currently, I have a collection of PDFs stored on S3 and am in the process of developing an app that requires me to display these PDFs based on their object names. For instance, there is a PDF named "photosynthesis 1.pdf" located in the biology/ folder, and ...