Tiptap Studio

TypeScript and errors

The types shipped in the package's .d.ts, and how failures reach JavaScript.

The package ships a .d.ts generated by wasm-bindgen. It declares the TiptapDoc class and the module functions, plus hand-written types for the plain objects that cross the boundary.

Shipped types

Import them like any other type:

import type {
  Assoc,
  BlockInline,
  Change,
  DiffGranularity,
  DiffOptions,
  JSONContent,
  Mark,
  NormalizeOptions,
  PosContent,
  PosEdit,
  PosRange,
  Position,
  Range,
  ResolvedPos,
  TextPoint,
  Violation,
} from "@tiptap-studio/rusty-parser";

They are declared as follows:

export interface Mark { type: string; attrs?: Record<string, unknown>; [k: string]: unknown; }
export interface JSONContent {
  type?: string;
  attrs?: Record<string, unknown>;
  content?: JSONContent[];
  marks?: Mark[];
  text?: string;
  [k: string]: unknown;
}
export interface Position { child: number; offset: number; }
export interface Range { start: Position; end: Position; }
export type Change =
  | { op: "setAttr"; path: number[]; key: string; value: unknown }
  | { op: "removeAttr"; path: number[]; key: string }
  | { op: "setText"; path: number[]; text: string | null }
  | { op: "spliceText"; path: number[]; from: number; lenDel: number; insert: string }
  | { op: "setMarks"; path: number[]; marks: Mark[] | null }
  | { op: "setExtra"; path: number[]; key: string; value: unknown }
  | { op: "removeExtra"; path: number[]; key: string }
  | { op: "insert"; path: number[]; index: number; node: JSONContent }
  | { op: "remove"; path: number[]; index: number }
  | { op: "replace"; path: number[]; node: JSONContent }
  | { op: "move"; path: number[]; from: number; to: number };
export interface Violation { path: number[]; kind: unknown; child?: number; mark?: number; }
export interface NormalizeOptions {
  mergeAdjacentText?: boolean;
  removeEmptyText?: boolean;
  removeEmptyNodes?: boolean;
}
export interface TextPoint { path: number[]; offset: number; }
export interface ResolvedPos {
  pos: number;
  depth: number;
  path: number[];
  parentOffset: number;
  index: number;
  textOffset: TextPoint | null;
}
export interface PosRange { from: number; to: number; }
export interface BlockInline { block: number[]; inline: Position; }
export type Assoc = "left" | "right";
export type DiffGranularity =
  | "block"
  | "inline"
  | { smart: { replaceThreshold: number } };
export interface DiffOptions { text?: DiffGranularity; }
export type PosContent =
  | { type: "text"; text: string; marks?: Mark[] }
  | { type: "nodes"; nodes: JSONContent[] };
export type PosEdit =
  | { type: "insert"; pos: number; content: PosContent }
  | { type: "delete"; from: number; to: number }
  | { type: "replace"; from: number; to: number; content: PosContent }
  | { type: "addMark"; from: number; to: number; mark: Mark }
  | { type: "removeMark"; from: number; to: number; markType: string }
  | { type: "setBlockAttrs"; pos: number; attrs: Record<string, unknown> };

There are no declared types for the schema object or the toHTMLWith options. Their shapes are on the Schema and TiptapDoc pages.

Method signatures use any

wasm-bindgen types every parameter and return value that crosses the boundary as a plain JS value as any. For example:

export class TiptapDoc {
  private constructor();
  free(): void;
  [Symbol.dispose](): void;
  static fromJSON(value: any): TiptapDoc;
  toJSON(): any;
  addMark(path: any, mark_type: string, attrs: any): boolean;
  diffWith(other: TiptapDoc, options: any): any;
  // ...
}
export function mapPosition(pos: number, edits: any, assoc: any): number;

Primitive parameters and results are typed (string, number, boolean, TiptapDoc), and childCount returns number | undefined. Everything else is any, so annotate the values yourself to get checking:

const json: JSONContent = doc.toJSON();
const changes: Change[] = a.diffWith(b, { text: "inline" } satisfies DiffOptions);
const edits: PosEdit[] = [{ type: "delete", from: 1, to: 3 }];
const patch: Change[] = doc.applyPosEdits(edits);
const found: number[][] = doc.pathsByType("heading");

Optional parameters are declared required

Parameters that accept undefined, such as attrs on addMark, marks on insertText, options on diffWith and assoc on mapPosition, are declared without ?. Plain JavaScript can leave them out. TypeScript reports a missing argument, so pass undefined:

doc.addMark([0, 0], "bold", undefined);
doc.insertText([0], { child: 0, offset: 0 }, "Hi ", undefined);
mapPosition(10, edits, undefined); // same as "left"

Errors

Every method and function that can fail throws a standard JS Error. TypeScript does not declare which ones throw. The message carries the Rust error's text:

SourceExample message
JSON parsingjson error: EOF while parsing an object at line 1 column 1
An argument of the wrong shapeError: unknown variant `chars`, expected one of `block`, `inline`, `smart`
A path-addressed edit on a missing nodeno node at path
Flat positionspos: 99 out of range (document size 20)
Inline rangesrange: offset 99 out of range in child 0
Block structureblock: no previous sibling to join at [0]
Position editspos-edit: overlapping edit at [2,4)
Change listsapply: no node at path [9]
A freed TiptapDocnull pointer passed to rust

Messages are meant for people and logs. Do not parse them: catch the error, and decide what to do from the call that failed.

try {
  doc.applyPosEdits(edits);
} catch (err) {
  console.warn("AI edit rejected:", (err as Error).message);
}

Methods that look something up do not throw when it is missing. They return undefined: nodeAt, firstByType, childCount and removeChild. mapPath returns null when the node is gone.

On this page