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

Ways to retrieve the identifier of a specific element within an array

After successfully retrieving an array of items from my database using PHP as the backend language, I managed to display them correctly in my Ionic view. However, when I attempted to log the id of each item in order to use it for other tasks, it consistent ...

Looking to execute a service method within an authguard service?

I am a beginner with Angular and I am looking to invoke a service method within the authguard service. The specific service function that I need is as follows. Please note that I do not want to make any changes to this service function. loadOrganizations() ...

Checking JavaScript files with TSLint

After spending many hours attempting to make this work, I still haven't had any success... I am wondering: How can I utilize TSLint for a .js file? The reason behind this is my effort to create the best possible IDE for developing numerous JavaScrip ...

A loop in JavaScript/TypeScript that runs precisely once every minute

Here is a snippet of my code: async run(minutesToRun: number): Promise<void> { await authenticate(); await this.stock.fillArray(); await subscribeToInstrument(this, this.orderBookId); await subscribeToOrderbook(this, this.orderBookId ...

Next.js: Generating static sites only at runtime due to getStaticProps having no data during the build phase, skipping build time generation

I am looking to customize the application for individual customers, with a separate database for each customer (potentially on-premise). This means that I do not have access to any data during the build phase, such as in a CI/CD process, which I could use ...

The function did not return a Promise or value as expected when using async and await

    I have been working on implementing this code structure for my cloud functions using httpRequest. It has worked seamlessly with those httpRequest functions in the past. However, I recently encountered an error when trying to use it with the OnWrite ...

Utilizing Angular 9's inherent Ng directives to validate input components within child elements

In my current setup, I have a text control input component that serves as the input field for my form. This component is reused for various types of input data such as Name, Email, Password, etc. The component has been configured to accept properties like ...

Exploring the power of makeStyles in Material UI when combined with TypeScript

I am currently in the process of converting a JavaScript template to Typescript. Here is an example of my accordionStyle.ts file: import { primaryColor, grayColor } from "../../material-dashboard-pro-react"; const accordionStyle = (theme?:an ...

The name '__DEV__' is not discoverable at the moment

While working with the mobx library in my project, I encountered an issue after installing it using npm. Upon exploring the mobx/src/error.ts file within the node_modules folder, I came across a compile time error on line 78: const errors: typeof niceError ...

Functions have been successfully deployed, but they are not appearing on the Azure Portal

I am experiencing difficulties deploying basic Typescript functions to Azure. Despite a successful deployment using VS code and confirmation in the Output window, I cannot see any functions listed in the Azure Portal under the Function App: https://i.stac ...

How can I implement a button in Angular Ag Grid to delete a row in a cell render

I have included a button within a cell that I want to function as a row deleter. Upon clicking, it should remove the respective row of data and update the grid accordingly. Check out the recreation here:https://stackblitz.com/edit/row-delete-angular-btn-c ...

Error: 'next' is not defined in the beforeRouteUpdate method

@Component({ mixins: [template], components: { Sidebar } }) export default class AppContentLayout extends Vue { @Prop({default: 'AppContent'}) title: string; @Watch('$route') beforeRouteUpdateHandler (to: Object, fro ...

You cannot call this expression. The type 'String' does not have any call signatures. Error ts(2349)

Here is the User class I am working with: class User { private _email: string; public get email(): string { return this._email; } public set email(value: string) { this._email = value; } ...

Ensure that the query value remains constant in Express.js

Issue: The query value is changing unexpectedly. // url: "http://localhost:4000/sr?q=%C3%BCt%C3%BC" export const search = async (req: Request, res: Response) => { try { const query = String(req.query.q) console.log("query: &quo ...

Is it possible to design a Typescript type that only contains one property from a defined set and is indexable by that set as well?

I have the different types listed below: type OrBranch = { or: Branch[] } type AndBranch = { and: Branch[] } I need a type called Branch that can either be an OrBranch or an AndBranch. I initially attempted this: type Branch = AndBrand | OrBranch ...

What is the best way to export a default object containing imported types in TypeScript?

I am currently working on creating ambient type definitions for a JavaScript utility package (similar to Lodash). I want users to be able to import modules in the following ways: // For TypeScript or Babel import myutils from 'myutils' // myuti ...

Connecting the SelectedItem of a listbox in ngPrime to an Observable Collection in Angular2

I am facing an issue while trying to use the ngPrime listbox in Angular2. I have a scenario where I am fetching an array of objects from Firebase as an observable and attempting to display it correctly in the listbox. <div *ngIf="addContactDialogVisibl ...

Extending the Object prototype within an ES6 module can lead to errors such as "Property not found on type 'Object'."

There are two modules in my project - mod1.ts and mod2.ts. //mod1.ts import {Test} from "./mod2"; //LINE X interface Object { GetFooAsString(): string; } Object.prototype.GetFooAsString = function () { return this.GetFoo().toString(); } //mod2. ...

Incorporating a new function into a TypeScript (JavaScript) class method

Is it possible to add a method to a method within a class? class MyClass { public foo(text: string): string { return text + ' FOO!' } // Looking for a way to dynamically add the method `bar` to `foo`. } const obj = new MyCl ...

A comprehensive guide on enabling visibility of a variable within the confines of a function scope

In the code snippet shown below, I am facing an issue with accessing the variable AoC in the scope of the function VectorTileLayer. The variable is declared but not defined within that scope. How can I access the variable AoC within the scope of VectorTile ...