You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在TypeScript中实现类似Tiptap的方法链式调用?以计算器场景为例

Understanding Tiptap-Style Chaining in TypeScript (Calculator Example)

Great question! Let's break down how to implement this flexible chaining pattern in TypeScript—mirroring Tiptap's editor.chain().focus().toggleItalic().run() API. We'll start with a basic calculator implementation, then expand it to support extensible commands just like Tiptap's extension system.


Core Pattern Recap

First, let's align on what makes Tiptap's chain API work:

  • chain() creates an isolated context that collects pending commands
  • Each command method (like toggleItalic()) adds an operation to a queue and returns the same context for continuous chaining
  • run() executes all queued commands in order and returns the final result

For our calculator, we want the exact same behavior: calculator.chain().setValue(10).add(3).sub(12).run() should return 1 when we call run().


Step 1: Basic Calculator Implementation

Let's build a minimal working version first. We'll use an isolated state and command queue for each chain call to avoid cross-contamination.

Define Types

First, set up TypeScript types for our state and commands:

// Holds the calculator's current value (isolated per chain)
type CalculatorState = {
  value: number;
};

// A command is a function that modifies the calculator state
type CalculatorCommand = (state: CalculatorState) => void;

Build the Calculator

Implement the calculator object with the chain API:

const calculator = {
  chain() {
    // Initialize fresh state and command queue for this chain
    const state: CalculatorState = { value: 0 };
    const commands: CalculatorCommand[] = [];

    // The chainable object that exposes our commands
    const chainable = {
      setValue(num: number) {
        commands.push((state) => { state.value = num; });
        return chainable; // Return self to enable chaining
      },
      add(num: number) {
        commands.push((state) => { state.value += num; });
        return chainable;
      },
      sub(num: number) {
        commands.push((state) => { state.value -= num; });
        return chainable;
      },
      run() {
        // Execute all queued commands in sequence
        commands.forEach(cmd => cmd(state));
        return state.value;
      }
    };

    return chainable;
  }
};

// Test it out!
console.log(calculator.chain().setValue(10).add(3).sub(12).run()); // Output: 1

Each chain() call creates its own state and queue, so multiple chains can run independently without interfering with each other.


Step 2: Extensible Version (Like Tiptap's Extensions)

Tiptap's real power comes from custom extensions that add new commands. Let's replicate this by making our calculator extensible with type-safe commands.

Define Extensible Types

Use TypeScript interfaces and generics to support type-safe extensions:

// Base interface for calculator commands (empty, designed for extension)
interface CalculatorCommands {}

// Map command functions to chainable methods (preserves type hints)
type ChainableCommands<T extends CalculatorCommands> = {
  [K in keyof T]: T[K] extends (...args: infer Args) => void
    ? (...args: Args) => Chainable<T>
    : never;
};

// Full chainable type: includes all commands + the run() method
type Chainable<T extends CalculatorCommands> = ChainableCommands<T> & {
  run: () => number;
};

Create a Factory for Extensible Calculators

Build a factory function that accepts extensions to add new commands dynamically:

function createCalculator<T extends CalculatorCommands>(
  extensions?: Record<keyof T, (state: CalculatorState) => (...args: any[]) => void>
) {
  return {
    chain(): Chainable<T> {
      const state: CalculatorState = { value: 0 };
      const commands: CalculatorCommand[] = [];

      // Base commands (setValue, add, sub)
      const baseCommands = {
        setValue: (state: CalculatorState) => (num: number) => {
          state.value = num;
        },
        add: (state: CalculatorState) => (num: number) => {
          state.value += num;
        },
        sub: (state: CalculatorState) => (num: number) => {
          state.value -= num;
        }
      };

      // Merge base commands with user-provided extensions
      const allCommands = { ...baseCommands, ...extensions } as Record<keyof ChainableCommands<T>, any>;

      // Build the chainable object
      const chainable: any = {
        run() {
          commands.forEach(cmd => cmd(state));
          return state.value;
        }
      };

      // Attach all command methods to the chainable object
      Object.keys(allCommands).forEach(key => {
        chainable[key] = (...args: any[]) => {
          commands.push(allCommands[key](state)(...args));
          return chainable;
        };
      });

      return chainable as Chainable<T>;
    }
  };
}

Add Custom Extensions (e.g., Multiply & Divide)

Define a new set of commands and create an advanced calculator:

// Define new multiplication/division commands
interface MultiplicativeCommands extends CalculatorCommands {
  mul: (num: number) => void;
  div: (num: number) => void;
}

// Create a calculator with the new commands
const advancedCalculator = createCalculator<MultiplicativeCommands>({
  mul: (state) => (num: number) => {
    state.value *= num;
  },
  div: (state) => (num: number) => {
    if (num === 0) throw new Error("Division by zero!");
    state.value /= num;
  }
});

// Test the extended calculator
console.log(advancedCalculator.chain().setValue(10).mul(2).div(5).run()); // Output: 4

Key Principles

  • Isolated Context: Each chain() call creates a fresh state and queue, so chains don't interfere with one another.
  • Command Queuing: Commands are stored until run() is called, ensuring ordered execution.
  • Type-Safe Extensions: Using TypeScript interfaces and generics lets us add new commands with full auto-completion and type checks, just like Tiptap.
  • No Class Required: We used plain objects and functions here, matching Tiptap's non-class-based implementation.

内容的提问来源于stack exchange,提问作者Ishan Chhabra

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 09:43:12