Search code examples
typescriptchaining

Typescript :: Conditional chaining function


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.


Solution

  • 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