Search code examples
javascripttypescriptmethod-chaining

How to reuse chained functions Javascript + Typescript


I'm using Typescript and creating a form validation library with chained methods, but I'm stuck trying to reuse functions because of the this return, I will exemplify in a simpler way:

const schema = {
  computer() {
    return {
      turnOn() {
        console.log('turn on')
        return this
      },
      openVscode() {
        console.log('open vscode')

        return this
      },
      work() {
        console.log('work')
        return this
      },
      turnOff() {
        console.log('turn off')
        return this
      }
    }
  },
  phone() {
    return {
      turnOn() {
        console.log('turn on')
        return this
      },
      takeSelfies() {
        console.log('take selfies')

        return this
      },
      callToMom() {
        console.log('call to mom')
        return this
      },
      turnOff() {
        console.log('turn off')
        return this
      }
    }
  }
}

everything works fine.

const devices = {
  iMac: schema.computer().turnOn().openVscode().work().turnOff(),
  iPhone: schema.phone().turnOn().takeSelfies().callToMom().turnOff()
}

but when I try to separate the repeated functions in another file, for example

const mixed = {
  turnOn() {
    console.log('turn on')
    return this
  },
  turnOff() {
    console.log('turn off')
    return this
  }
}

to reuse

const newSchema = {
  computer() {
    return {
      ...mixed,
      openVscode() {
        console.log('open vscode')

        return this
      },
      work() {
        console.log('work')
        return this
      }
    }
  },
  phone() {
    return {
      ...mixed,
      takeSelfies() {
        console.log('take selfies')

        return this
      },
      callToMom() {
        console.log('call to mom')
        return this
      }
    }
  }
}

I'm stuck

enter image description here

I know this is because the this of the mixed object returns only the content itself, but I don't know how to solve this problem.

I thank you all!


Solution

  • The points made by captain-yossarian and Charlie about mixins are good and worth investigating if you want use them. However, here's a solution that doesn't require much refactoring of the original code:

    const mixed = { /* same as before */ }
    
    type AnyFn = (...args: never[]) => unknown
    
    type SetReturnTypes<T extends Record<string, AnyFn>> = {
      [K in keyof T]: (...args: Parameters<T[K]>) => SetReturnTypes<T>
    }
    
    const makeMixed = <T>(obj: T & ThisType<T & typeof mixed>): SetReturnTypes<T & typeof mixed> => ({
      ...mixed,
      ...obj
    }) as unknown as SetReturnTypes<T & typeof mixed>
    
    const newSchema = {
      computer() {
        return makeMixed({
          openVscode() {
            console.log('open vscode')
            return this
          },
          work() {
            console.log('work')
            return this
          }
        })
      },
      phone() {
        return makeMixed({
          takeSelfies() {
            console.log('take selfies')
            return this
          },
          callToMom() {
            console.log('call to mom')
            return this
          }
        })
      }
    }
    
    newSchema.computer().turnOn().openVscode()
    
    • SetReturnTypes makes all methods of T return the correct this type, assuming all these methods return this.
    • makeMixed is a helper to create an object with mixed with the correct types. It uses ThisType so that the methods in obj have the correct this type:
    makeMixed({
      doSomething() {
        this.turnOn() // would be a compile error without the ThisType
        return this
      }
    })
    

    An issue with this is that methods not returning this are incorrectly typed. If you want to ensure that all the methods return this, let me know and I may be able to come up with a solution for that.

    Playground link