Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

310
Views
How should I go about method chaining in TypeScript (like how Tiptap does it)

Tiptap (and many other text editors based on ProseMirror) has this amazing API where we can chain commands.

For example:

editor.chain().focus().toggleItalic().run()

Here, calling chain() returns all the chainable commands which themselves return chainable commands and that's how the chain goes on and on until you call run().

After calling run(), the commands between chain() and run() are executed in sync.

What's interesting (as far as my knowledge goes) is that this isn't a class. All of this can also be extended (including typings) by creating custom extensions for Tiptap. See this bold extension and its types.

I did go thru there source but wasn't able to comprehend much.

I really liked this design pattern and was wondering how should I go about implementing this in general...

Say, I wanted to implement a calculator on this pattern. How can I make this line work?

calculator.chain().setValue(10).add(3).sub(12).run() // 1

Thank you!

about 4 years ago · Juan Pablo Isaza
2 answers
Answer question

0

There's lots of ways you could potentially implement this, here is just one with a simple object. A class encapsulation would propbably be a good idea. So long as you keep your methods contained within a single object, you can keep returning it and adding to the callback queue, which will eventually be executed on .run() and break the loop.

const calculator = {
  state: { value: 0 },
  callbacks: [] as ((...args: any[]) => any)[],
  _exec: {
    setValue: (val: number) => (calculator.state.value = val),
    add: (val: number) => (calculator.state.value += val),
    sub: (val: number) => (calculator.state.value -= val),
  },
  _queue: {
    setValue: (val: number) => {
      calculator.callbacks.push(() => calculator._exec.setValue(val));
      return calculator._queue;
    },
    add: (val: number) => {
      calculator.callbacks.push(() => calculator._exec.add(val));
      return calculator._queue;
    },
    sub: (val: number) => {
      calculator.callbacks.push(() => calculator._exec.sub(val));
      return calculator._queue;
    },
    run: () => {
      calculator.callbacks.forEach((cb) => cb());
      calculator.callbacks = [];
    },
  },
  chain: () => calculator._queue,
};

calculator.chain().setValue(10).add(3).sub(12).run();
console.log(calculator.state.value); // logs 1
about 4 years ago · Juan Pablo Isaza Report

0

Just use a class:

class Calculator {
  constructor(value) {
    this.value = value;
  }
  run() { return this.value; }
  add(x) { return new Calculator(this.value + x); }
  sub(x) { return new Calculator(this.value - x); }
  log() { console.log(this.value); return this; }
  static withValue(v) { return new this(v); }
}
const calc = Calculator.withValue(10).add(3).log();
console.log(calc.sub(12).run());
console.log(calc.add(29).run());

The only thing that is important for method chaining is to return an object with all the methods (here: a Calculator instance) from each of the chainable methods. You can achieve the same without class syntax as well:

function calculatorWithValue(v) {
  return {
    run() { return v; },
    add(x) { return calculatorWithValue(v + x); },
    sub(x) { return calculatorWithValue(v - x); },
    log() { console.log(v); return this; },
  };
}
const calc = calculatorWithValue(10).add(3).log();
console.log(calc.sub(12).run());
console.log(calc.add(29).run());

I recommend to keep the objects immutable, to allow creation of multiple independent chains or forking of an existing chain - see the examples above, where the second result is 10 + 3 + 29, not 10 + 3 - 12 + 29.

If you want the execution to be lazy, that's easy as well:

class Calculator {
  constructor(run) {
    this.run = run;
  }
  static withValue(v) { return new this(() => v); }
  add(x) { return new Calculator(() => this.run() + x); }
  sub(x) { return new Calculator(() => this.run() - x); }
  log() { return new Calculator(() => {
    const v = this.run();
    console.log(v);
    return v;
  }); }
}
const calc = Calculator.withValue(10).add(3).log();
console.log(calc.sub(12).run());
console.log(calc.add(29).run());

If you want to make this extensible with some kind of plugin architecture, just put more methods on the prototype, or build the object dynamically. For TypeScript, you'd use declaration merging to amend the Calculator interface with the signatures that your plugin provides.

about 4 years ago · Juan Pablo Isaza Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!