0

I understand that this may be an anti-pattern, but I'm curious how certain JavaScript libraries have the ability to chain JavaScript functions with optional parenthesis... (e.g. chalk)

An example of this would be...

let test = (new SomeClass()).initiate.parse(1).end;
let test = (new SomeClass()).initiate(1).parse.end;

Is there a way of doing this? I thought about maybe trying to do this with getters but the get initiate() of SomeClass was overridden by the class function initiate().

jscul
  • 748
  • 1
  • 10
  • 25
  • 5
    Why would you want to do this? This is nightmare material. – Brad Feb 25 '19 at 23:27
  • @Brad Quite a few libraries have this type of pattern, for example, in chalk you can chain settings properties: `chalk.blue.underline.bold('with a blue substring')`. I'm curious how you create a pattern like this. – jscul Feb 25 '19 at 23:32
  • I've seen plenty with chaining by calling methods, like `chalk.blue().underline().bold()`, where each method effectively returns `this`, but I haven't seen any that abuse getters to cause an action to occur. – Brad Feb 25 '19 at 23:46
  • @Brad yeah, it seems like a very weird pattern but in the chalk documentation itself there's some form of that. That's what I was curious about. Same with a lot of testing libraries, they have code like: `expect.to.be.equal(4)`. – jscul Feb 25 '19 at 23:52

2 Answers2

3

No, it is not possible to have a getter and a method with the same name.

You could achieve what you want though. One way is to use a Proxy in your getters to make it so that when a further property is accessed, that property of the original object is accessed instead.

class SomeClass {

  get initiate ( ) {
    console.log( `Accessed initiate` );
    return new Proxy( x => {
      console.log( `Called initiate with arg ${x}` );
      this.stored = x;
      return this;
    }, { get: (_,prop) => this[prop] } );
  }
  
  get parse ( ) {
    console.log( `Accessed parse` );
    return new Proxy( x => {
      console.log( `Called parse with arg ${x}` );
      this.stored = x;
      return this;
    }, { get: (_,prop) => this[prop] } );
  }
  
  get end ( ) {
    return this.stored;
  }
  
}

console.log( (new SomeClass).initiate(2).parse.end );
console.log( (new SomeClass).initiate.parse(3).end );
console.log( (new SomeClass).initiate(5).parse(7).end );
Paul
  • 139,544
  • 27
  • 275
  • 264
  • Brilliant answer... just as a follow-up: if I wanted to get rid of `.end` but still allow both `.parse` and `.parse(3)` (e.g. have them both return the class instance) how would I create the conditional to return `this` instead of `this[prop]` in the proxy method `get` for `parse`. Or is that not possible? – jscul Feb 26 '19 at 01:43
  • @jscul I'm busy atm, but if you ping me in about 14-20 hours we can go over it in a chat. `.parse` and `.parse(3)` do both evaluate to the instance of SomeClass. You might not need end if all you want as the final result is that instance, or if you can use a valueOf instance method or better [`[Symbol.toPrimitive]`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive) to achieve your end goal. – Paul Feb 26 '19 at 01:59
  • @jscul Great :) Glad I could help – Paul Feb 26 '19 at 19:34
2

I would heavily recommend taking a look at how chai or other assertion libraries do this. For the code, you're looking for, you can look here.

What they do is they define an Assertion class and define methods and properties separately. From there, they use getters as you mentioned to achieve what you are trying to do with properties but they also keep methods separate (see addMethod)

Another question you can look at is How does the expect().to.be.true work in Chai? where the accepted answer has a code example of how to get something very basic working.

I would be conscientious about having methods and properties the same name though. That can confuse a lot of people who are using your class as a consumer.

aug
  • 11,138
  • 9
  • 72
  • 93