6

I'm trying to implement a set of chained function but somehow I get stuck here.

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

interface ISpecialCalculator extends ISimpleCalculator {
  specialPlus(value: number): ISimpleCalculator;
  specialMinus(value: number): ISimpleCalculator;
}

let testCalculator: ISpecialCalculator;
testCalculator  
  .plus(20)
  .multiply(2)
  .specialPlus(40)  
  .plus(20)
  .minus(5)
  .specialMinus(20)  //<-- Error! Property 'specialMinus' does not exist on type 'ISimpleCalculator'.
  .sum()

I want to archive type check of the function in the chain. In the above example, I want the functions specialPlus and specialMinus in ISpecialCalculator to be used once only and ISimpleCalculator can be used for multiple times. I'm pretty fresh to the typescript and I've been trying different approaches (Advanced type (Pick & Omit)) with no success so far. I want to know is there any other way to help in this case.

3
  • I don't think this is possible in typescript, you will need javascript runtime implementation for such functionality Commented Dec 10, 2019 at 8:00
  • Imo you have to implment a concrete class, then it is working fine: typescriptlang.org/play/index.html#code/… Commented Dec 10, 2019 at 8:10
  • Hi @r3dst0rm, actually it's the same result. Don't forget the ISpecialCalculator interface contains of more than one function and each of them is allowed to be used once only. Commented Dec 10, 2019 at 8:30

3 Answers 3

3

Removing the some functions is simple, you can just use Omit<this, 'specialPlus'> If we test this it almost works, if you call specialPlus you will get an error if you call it immediately after another call to specialPlus, you can however call it after a call to specialMinus

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

This is because Omit will work on the this type bound when testCalculator is declared, so specialMinus will return in fact Omit<ISpecialCalculator, 'specialMinus'> which will still contain specialPlus even though we previously removed it. What we want is for Omit to work on the type of this returned by the previous function. We can do this if we capture the actual type of this for each call using a generic type parameter, and Omit methods from this type parameter not from 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

Sign up to request clarification or add additional context in comments.

3 Comments

Hi @Titian, I have tried the same approach as yours before. Just like what you said, it's "almost" works :) but don't forget we still can add in plus minus ... in between of specialPlus & specialMinus.
@NelsonTang wops, my bad. Fixed. You need to apply the same generic treatment to the other functions as well just that they return TThis
OH!!!! I JUST GET IT! You are so right! What I want is this returned by the previous function. I've underestimated the power of this. You have saved my day master @Titian!
1
specialPlus(value: number): ISimpleCalculator;

When you call this function, you are getting back a simple calculator that doesn't have the special functions anymore. The special interface should also return this and it should be working:

interface ISpecialCalculator extends ISimpleCalculator {  
   specialPlus(value: number): this;
   specialMinus(value: number): this;
}

1 Comment

Hi @amit, the reason I put ISimpleCalculator is that I want to limit the functions in ISpecialCalculator to be used once only.
-1

Try the following (full code tested based on the question):

interface ISimpleCalculator {
  plus(value: number): this
  minus(value: number): this
  divide(value: number): this
  multiply(value: number): this
  sum(): void
}

interface ISpecialCalculator extends ISimpleCalculator {
  specialPlus(value: number): this
  specialMinus(value: number): this
}

let testCalculator: ISpecialCalculator
testCalculator
  .plus(20)
  .multiply(2)
  .specialPlus(40)
  .plus(20)
  .minus(5)
  .specialMinus(20) 
  .sum()

If you want to limit the special[Plus|Minus] usage then you can accomplish that in the concrete class that implements the ISpecialCalculator interface.

The code below may give you some ideas:

class Calculator implements ISpecialCalculator {
  specialPlusUsed = false
  specialMinusUsed = false

  specialPlus(value: number): this {
    if (this.specialPlusUsed) throw new Error("SpecialPlus can be used only once!")

    this.specialPlusUsed = true
    // Related calculations here...
    return this
  }

  specialMinus(value: number): this {
    if (this.specialMinusUsed) throw new Error("SpecialMinus can be used only once!")
    this.specialMinusUsed = true
    // Related calculations here...
    return this
  }

  plus(value: number): this {
    // Related calculations here...
    return this
  }

  minus(value: number): this {
    // Related calculations here...
    return this
  }

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

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

  sum(): void {
    // Related calculations here...
  }
}

4 Comments

Hi Ben Dev, your answer is the same as above @amit. The function in ISpecialCalculator is allowed to be used only once.
@NelsonTang interfaces define a contract, you cannot limit how many times they will be used within the application!
@NelsonTang take a look at the sample code that implements the "ISpecialCalculator" interface. Hope that helps.
Hi @BenDev, I fully understand at your approach but what I'm trying to achieve is blocking the user through the type guard, not at runtime.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.