Prevent methods from being called in a Typescript class after they have already

I encountered a scenario where I need to exclude certain methods from the return type of a class method once they have been called. Consider a class named Setup with methods step1, step2, and step3.

class Setup {

    step1() {
        return this;
    }
    
    step2() { 
        return this;
    }
    
    step3() { 
        return this;
    }
}

let setup = new Setup();

Here is my requirement:

  1. Once step1 is executed, it should return an instance of Setup without the step1 method, allowing users to only choose between step2 and step3. Similarly, after executing step2, only step3 should be available.
  2. The order of method execution should not matter; for example, calling step3 before step1 should work.
  3. The solution needs to be dynamic, ensuring that a method once invoked becomes unavailable for further invocation during runtime.
let setup = new Setup();

setup
    .step1()
    .step2()
    .step1(); // This should be prevented as step 1 was already executed

I have attempted to achieve this by using the Omit type but faced issues where invoking step2 still displayed step1 as an option. This is mainly because Omit excludes keys based on the class type rather than the current instance or method being called.

export type Omit<A extends object, K extends string> = Pick<A, Exclude<keyof A, K>>

class Setup {

    step1(): Omit<Setup, 'step1'> {
        return this;
    }
    
    step2(): Omit<Setup, 'step2'>{ 
        return this;
    }
    
    step3():Omit<Setup, 'step3'>{ 
        return this;
    }
}

let setup = new Setup();

Answer №1

To achieve the goal of triggering a compiler warning in TypeScript if a method is called more than once, as well as generating a runtime error for repeated method calls, separate efforts need to be made for each objective. Unfortunately, the compiler cannot automatically enforce constraints at compile time based on runtime behavior. Therefore, we need to address these aspects individually.


Starting with the type system:

type OmitSetup<K extends string> = Omit<Setup<K>, K>;
declare class Setup<K extends string = never> {
  step1(): OmitSetup<K | "step1">;
  step2(): OmitSetup<K | "step2">;
  step3(): OmitSetup<K | "step3">;
}

The key idea here is to utilize generics in the Setup class by defining a constrained type parameter K representing the union of method names that should not be allowed. By default, K = never, indicating that initially no method names are restricted.

Additionally, even if methods like step1, step2, and step3 are declared in Setup<K>, they will always exist regardless of the specific value of

K</code. To handle this issue, we introduce <code>OmitSetup<K>
, which filters out these methods using the Omit utility type. As each method is called, the compiler modifies the return type by adding the new method name to the suppressed list.

At compilation time, consider the following scenario:

const s = new Setup();
// const s: Setup<never>
const s1 = s.step1();
// const s1: OmitSetup<"step1">
const s12 = s1.step2();
// const s12: OmitSetup<"step1" | "step2">
const s123 = s12.step3();
// const s123: OmitSetup<"step1" | "step2" | "step3">

This approach demonstrates how each call further restricts the available methods until eventually all methods are suppressed.

By combining types and runtime behavior, desired constraints can be enforced:

s.step1().step2().step3(); // okay
s.step2().step1().step3(); // okay
s.step1().step2().step1(); // error!
// -------------> ~~~~~
// Property 'step1' does not exist on type 'OmitSetup<"step1" | "step2">'. 
// Did you mean 'step3'?

Moving on to the runtime aspect:

class Setup {
  step1() {
    console.log("step1");
    return Object.assign(new Setup(), this, { step1: undefined });
  }
  step2() {
    console.log("step2");
    return Object.assign(new Setup(), this, { step2: undefined });
  }
  step3() {
    console.log("step3");
    return Object.assign(new Setup(), this, { step3: undefined });
  }
}

In this implementation, each method returns a new object, preventing method re-calls by explicitly setting them to undefined. Testing confirms the runtime behavior:

const s = new Setup();
s.step1().step2().step3(); // "step1", "step2", "step3"
s.step2().step1().step3(); // "step2", "step1", "step3"
s.step1().step2().step1(); // "step1", "step2", RUNTIME ERROR!
// s.step1().step2().step1 is not a function

These results demonstrate the prevention of calling the same method multiple times during execution.


Lastly, unifying both type declarations and runtime functionality in a single TypeScript file:

type OmitSetup<K extends string> = Omit<Setup<K>, K>;
class Setup<K extends string = never> {
  step1(): OmitSetup<K | "step1"> {
    console.log("step1");
    return Object.assign(new Setup(), this, { step1: undefined }) as any
  }
  step2(): OmitSetup<K | "step2"> {
    console.log("step2");
    return Object.assign(new Setup(), this, { step2: undefined }) as any
  }
  step3(): OmitSetup<K | "step3"> {
    console.log("step3");
    return Object.assign(new Setup(), this, { step3: undefined }) as any
  }
};

This version emphasizes annotating method return types and ensuring loose typing with "as any", highlighting the independence between implementation and typings. This explicit approach aids clarity since the compiler may not infer complex relationships between data structures accurately.


Playground link to code

Answer №2

Revise

In light of your explanation that the goal is to eliminate the function itself, simply reassign it to undefined, effectively erasing the function.

Additionally, you can enhance Typescript inference by appropriately excluding multiple types.

class Setup {
    step1(): Omit<Setup, 'step1'> {
        (this as any).step1 = undefined;
        return this;
    }
    
    step2(): Omit<Setup, 'step1' | 'step2'> { 
        return this;
    }
    
    step3(): Omit<Setup, 'step1' | 'step2' | 'step3'> { 
        return this;
    }
}

This approach ensures that the step1 function is effectively removed, inaccessible for invocation and will trigger a TypeError: step1 is undefined. Furthermore, Typescript will correctly display only step2 and step3.

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 preventing me from assigning a value of false to my JavaScript variable?

I'm encountering an issue where the articleExists variable is not being set to true on line 6, even though I have used console logs to double check that the if statement containing it is functioning properly. app.post("/articles", function(req, res) ...

Node.Js Web Scraping: How to Extract Data from JavaScript-Rendered Pages Using Request

I am looking to extract specific content from Google search results that is only visible in browsers, potentially due to Javascript being enabled. Specifically, I am interested in scraping the Knowledge Graph "People also search for" information. Currentl ...

The app's connection issue persists as the SDK initialization has exceeded the time limit

We are currently facing an issue when trying to publish a new manifest for our app in the store. The Microsoft team in India is encountering an error message that says "There is a problem reaching the app" during validation. It's worth noting that th ...

The click event triggered by the onclick clone/function may not always activate the click handler

As a newcomer in the JavaScript domain, I am encountering an issue where the first clone created after clicking 'add more' does not trigger my click me function. However, every subsequent clone works perfectly fine with it. What could be causing ...

Encountering the error "Cannot read property 'header' of undefined" while conducting tests on Nodejs using supertest

In my server.js file, I have set up my express app. I tried to run a demo test in my test file using express, but unfortunately, the test did not run successfully. const request = require('supertest'); const express = require('express' ...

Struggling with integrating Meteor.wrapAsync with the mailchimp-api-v3

I am currently exploring the use of mailchimp-api-v3 in a Meteor project (1.4.1.3) and I particularly like the batch support it offers. To make the call, I have enclosed it within Meteor's .wrapAsync function (which had a bit of a learning curve, but ...

What is the best way to clear the selected option in a dropdown menu when choosing a new field element?

html <div class="row-fluid together"> <div class="span3"> <p> <label for="typeofmailerradio1" class="radio"><input type="radio" id="typeofmailerradio1" name="typeofmailerradio" value="Postcards" />Postcards& ...

Expand the clickable area of the checkbox

Is it possible to make clicking on an empty space inside a table column (td) with checkboxes in it check or uncheck the checkbox? I am limited to using only that specific td, and cannot set event handlers to the surrounding tr or other tds. The functional ...

Creating dynamic and fluid motion with Bezier curves on canvas

I am currently working on creating a line that spans across the canvas from left to right. In these early stages of development, I am using a step-by-step animation approach with the following function: timer = window.setInterval(draw_line, 30); Here is ...

Deactivating an emitted function from a child component in Angular 4

There is a main component: @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { funcBoo():void{ alert("boo"); //return fal ...

How can I effectively monitor and track modifications to a document's properties in MongoDB?

I'm wondering how to effectively track the values of a document in MongoDB. This involves a MongoDB Database with a Node and Express backend. For example, let's say there is a document within the Patients collection: { "_id": "4k2lK49938d ...

What is the method for retrieving an array or object that contains a collection of files designated for uploading within the jQuery file upload plugin?

Currently, I have successfully integrated a form into my Rails site and set up the jQuery file upload plugin. The upload form functions properly with the ability to select multiple files and utilize all ajax upload features. However, a challenge I am faci ...

Unable to attach 'gridOptions' as it is not a recognized attribute of 'ag-grid-angular' component (Angular4)

I am facing an issue with my HTML code and Angular components: <ag-grid-angular [gridOptions]="gridOptions"></ag-grid-angular> My component code is as follows: import {GridOptions} from 'ag-grid'; ... export class SampleComponent ...

Encountered error message: "Cannot assign argument of type '() => () => boolean' to parameter of type 'EffectCallback'"

I recently started working with TypeScript. I encountered an issue when attempting to utilize useEffect in TypeScript within a React context, Error: Argument of type '() => () => boolean' is not assignable to parameter of type 'Effec ...

What is the best way to sequentially invoke methods in Angular?

When initializing in the ngOnInit() method, I need to call several methods synchronously. However, all these methods involve asynchronous calls to an API. The challenge is that certain variables must be set before proceeding with the subsequent methods. Un ...

Sending data from Django's render() method to React.js

Currently, I'm working on a Django + React Application project where I am faced with the challenge of passing JSON data from Django's render() function to React.js. To achieve this, I initiate the rendering of an HTML page using Django, with the ...

While trying to install express using Node.js, an error (error:0906D06C :PEM routines : PEM_read_bio npm) occurred

I recently installed node.js on my computer and npm came along with it. However, when I try to execute "npm install express" in my Windows command prompt, I encounter the following error message. Can anyone guide me on how to resolve this issue? C:\U ...

The ScrollToTop feature in MUI component seems to be malfunctioning when paired with the useScrollTrigger hook

I am in the process of developing a custom ScrollToTop component using MUI's useScrollTrigger hook. More information can be found at this link Feel free to check out the sample code here: https://codesandbox.io/s/stackoverlow-mui-usescrolltrigger-er9 ...

Autofill Dropdown in AngularJS Using Data from Previous Page

Despite searching extensively on StackOverFlow, I was unable to find the answer I needed. So, I am posting my question below. I have a form that includes a dropdown menu. When a user clicks a button, they are taken to a new HTML page with additional infor ...

Is it possible to incorporate the arrow function within the debounce function?

export const debounce = (callback: Function, ms = 300) => { let timeoutId: ReturnType<typeof setTimeout> return function (...args: any[]) { clearTimeout(timeoutId) timeoutId = setTimeout(() => callback.apply(this, args), ms) ...