Implementing a conditional chaining function in TypeScript

I'm currently facing an issue while implementing a set of chained functions.

interface IAdvancedCalculator {
  add(value: number): this;
  subtract(value: number): this;
  divideBy(value: number): this;
  multiplyBy(value: number): this;
  calculate(): void
}

interface ISpecialAdvancedCalculator extends IAdvancedCalculator {
  specialAdd(value: number): IAdvancedCalculator;
  specialSubtract(value: number): IAdvancedCalculator;
}

let myCalculator: ISpecialAdvancedCalculator;
myCalculator
  .add(20)
  .multiplyBy(2)
  .specialAdd(40)
  .subtract(5)
  .specialSubtract(20) //<-- Error! Property 'specialSubtract' does not exist on type 'IAdvancedCalculator'.
  .calculate()

I am aiming to ensure type checking for the functions in the chain. Specifically, I want specialAdd and specialSubtract functions defined in ISpecialAdvancedCalculator to be used only once each, while IAdvancedCalculator functions can be used multiple times. As a TypeScript beginner, I have tried various approaches like Advanced types (Pick & Omit) without any success. Are there any other solutions I can explore to address this scenario?

Answer №1

Eliminating certain functions can be done easily by using Omit<this, 'specialPlus'>. Testing this approach shows that it mostly works, unless you try to call specialPlus right after another call to it, in which case an error will occur. However, calling it after a call to specialMinus does not produce any issues.

interface ISpecialCalculator extends ISimpleCalculator {
  specialPlus(value: number): Omit<this, 'specialPlus'>;
  specialMinus(value: number): Omit<this, 'specialMinus'>;
}

declare let testCalculator: ISpecialCalculator;
testCalculator  
  .specialPlus(40)
   // .specialPlus(40) // error 🎉
  .specialMinus(20)
  .specialPlus(40) //ok 😢
  .sum()

Playground Link

The reason for this behavior is because Omit acts on the type of this when the testCalculator variable is declared. Consequently, the specialMinus function will actually return

Omit<ISpecialCalculator, 'specialMinus'>
, which still includes specialPlus even if it was previously omitted. To address this issue, we need to apply Omit based on the type returned by the preceding function, rather than on polymorphic this.

interface ISimpleCalculator {
  plus<TThis>(this: TThis,value: number): TThis;
  minus<TThis>(this: TThis,value: number): TThis;
  divide<TThis>(this: TThis,value: number): TThis;
  multiply<TThis>(this: TThis,value: number): TThis;
  sum(): void
}

interface ISpecialCalculator extends ISimpleCalculator {
  specialPlus<TThis>(this: TThis, value: number): Omit<TThis, 'specialPlus'>;
  specialMinus<TThis>(this: TThis, value: number): Omit<TThis, 'specialMinus'>;
}

declare let testCalculator: ISpecialCalculator;
testCalculator
  .specialPlus(40)
  // .specialPlus(40) // error 🎉
  .specialMinus(20)
  .plus(10)
  .specialPlus(40) // also error 🎉
  .plus(10)
  .sum()

Playground Link

Answer №2

customAddition(value: number): IAdvancedCalculator;

Upon invoking this method, you will receive an advanced calculator that no longer includes the special features. The customized interface must also return this and remain operational:

interface ICustomizedCalculator extends IAdvancedCalculator {  
   customAddition(value: number): this;
   customSubtraction(value: number): this;
}

Answer №3

Check out the complete code snippet tested according to the question:

interface ISimpleCalculation {
  add(value: number): this
  subtract(value: number): this
  divide(value: number): this
  multiply(value: number): this
  calculateSum(): void
}

interface ISpecializedCalculation extends ISimpleCalculation {
  specialAddition(value: number): this
  specialSubtraction(value: number): this
}

let exampleCalculator: ISpecializedCalculation
exampleCalculator
  .add(10)
  .multiply(3)
  .specialAddition(15)
  .subtract(5)
  .specialSubtraction(8) 
  .calculateSum()

If you wish to restrict the use of special[Addition|Subtraction], you can implement that in the concrete class that follows the ISpecializedCalculation interface.

The given code snippet below could provide some inspiration:

class CustomCalculator implements ISpecializedCalculation {
  hasUsedSpecialAddition = false
  hasUsedSpecialSubtraction = false

  specialAddition(value: number): this {
    if (this.hasUsedSpecialAddition) throw new Error("Special Addition can only be used once!")

    this.hasUsedSpecialAddition = true
    // Perform relevant calculations here...
    return this
  }

  specialSubtraction(value: number): this {
    if (this.hasUsedSpecialSubtraction) throw new Error("Special Subtraction can only be used once!")
    this.hasUsedSpecialSubtraction = true
    // Perform relevant calculations here...
    return this
  }

  add(value: number): this {
    // Perform relevant calculations here...
    return this
  }

  subtract(value: number): this {
    // Perform relevant calculations here...
    return this
  }

  divide(value: number): this {
    // Perform relevant calculations here...
    return this
  }

  multiply(value: number): this {
    // Perform relevant calculations here...
    return this
  }

  calculateSum(): void {
    // Perform relevant calculations here...
  }
}

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

What is the process of extracting an observable from another observable using the pipe method?

Is there a more efficient way to convert an Observable of Observables into an array of Observables in my pipe method? Here is the scenario: // The type of "observables" is Observable<Observable<MyType>[]> const observables = this.http.get<M ...

React-leaflet with TypeScript is failing to display GeoJSON Points on the map

I am attempting to display points on a map using geojson points in my react application with react-leaflet. However, for some unknown reason, the points are not rendering on the map. When I try importing a JSON file, I encounter an error: TS2322: Ty ...

Tips for choosing a specific quantity and adjusting its value

Just starting out with Ionic 3 and looking for some help with the code. Can anyone assist me in understanding how to change the value of an item in a shopping cart and have the subtotal reflect that change? cart.ts private _values1 = [" 1 ", "2", " 3 "," ...

Mismatched non-intersecting categories with TypeScript

I have an object that I need to conditionally render some JSX based on certain properties. I want to restrict access to specific parts of the object until certain conditions are met. Here is my scenario: const { alpha, bravo } = myObject; if (alpha.loadin ...

What are the Typescript object types where the keys are functions that take a single generic argument consistently?

Explaining this concept in English is quite challenging, but here's what I'm aiming for: const operations = { store: (input: T): T => { return input; }, discard: (input: T): void => { console.log(input); } } In both fun ...

Establishing the types of object properties prior to performing a destructuring assignment

Consider a scenario where a function is utilized to return an object with property types that can be inferred or explicitly provided: const myFn = (arg: number) => { return { a: 1 + arg, b: 'b' + arg, c: (() => { ...

Unit testing component in Ionic 2 with Ionic's specific markup and elements

Within my Angular 2 component for an Ionic 2 app, I utilize Ionic's markup as shown below: <ion-card> <h3>{{ rawcontent.name }}</h3> <p *ngIf="rawcontent.description">{{ rawcontent.description }}</p> </ion-car ...

Issue with event.stopPropagation() in Angular 6 directive when using a template-driven form that already takes event.data

I am currently developing a citizenNumber component for use in forms. This component implements ControlValueAccessor to work with ngModel. export class CitizenNumberComponent implements ControlValueAccessor { private _value: string; @Input() place ...

Retrieve functions with varying signatures from another function with precise typing requirements

My code includes a dispatch function that takes a parameter and then returns a different function based on the parameter value. Here is a simplified version: type Choice = 'start' | 'end'; function dispatch(choice: Choice) { switch ...

Having trouble executing the project using Gulp

I'm a beginner in front-end development and I am working on an existing project that I'm having trouble running. According to the documentation, I should run the project by executing: $ gulp && gulp serve But I keep getting this error: ...

Tips for integrating Typescript into a pre-existing Vue 3 project

The contents of my package.json file are as follows: { "name": "view", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve" ...

The onClick function for a button is not functioning properly when using the useToggle hook

When the button is clicked, it toggles a value. Depending on this value, the button will display one icon or another. Here is the code snippet: export const useToggle = (initialState = false) => { const [state, setState] = useState(initialState); c ...

Is there an equivalent concept to Java's `Class<T>` in TypeScript which allows extracting the type of a class constructor?

I'm in need of a feature similar to the Class functionality in Java, but I've had no luck finding it. Class<T> is exactly what I require. I believe it could be named NewableFunction<T>. However, such an option does not exist. Using M ...

Encountering an issue with PrimeNG's <p-calendar> component: the error message "date

I encountered an issue resulting in the following error message: core.es5.js:1020 ERROR Error: Uncaught (in promise): TypeError: date.getMonth is not a function TypeError: date.getMonth is not a function This error occurs whenever I attempt to implement ...

Stopping the subscription to an observable within the component while adjusting parameters dynamically

FILTER SERVICE - implementation for basic data filtering using observables import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { Filter } from '../../models/filter.model'; imp ...

Determining the output type by considering the presence of optional parameters

function customFunction<T>(defaultValue?: T) { return defaultValue; } const definitelyNullOrUndefined = customFunction<string>(); // type: string | undefined const definitelyStringType = customFunction<string>('foobar'); // ...

Can anyone explain why the Splice function is removing the element at index 1 instead of index 0 as I specified?

selectedItems= [5,47] if(this.selectedItems.length > 1) { this.selectedItems= this.selectedItems.splice(0,1); } I am attempting to remove the element at index 0 which is 5 but unexpectedly it deletes the element at index ...

Is there a way to search for text and highlight it within an HTML string using Ionic

Welcome everyone! I am currently utilizing Ionic to load an article from a local HTML string. <ion-content padding> <p [innerHTML]="url"></p> </ion-content> The 'url' variable contains the local HTML string. My goal ...

Utilizing a Function's Parameter Type in TypeScript: A Beginner's Guide

Currently, I am creating a custom method that wraps around Angular's HttpClient service. I want users to have the ability to pass in options, but I am struggling to find the proper way to reference that type in my method parameter definition. For exa ...

Incorporating a sidemenu into a DOM using Ionic2 and Angular2 Typescript

I am currently struggling to properly integrate the sidemenu from the app.ts file. app.html: <ion-menu [content]="content"></ion-menu> <ion-nav id="nav" [root]="rootPage" #content ></ion-nav> app.ts import {App, IonicApp,Page, ...