Skip to main content

TypeScript Support

Tiptap is written in TypeScript and provides full type safety out of the box. This guide shows you how to leverage TypeScript’s features when working with Tiptap.

Editor Type

The Editor class is fully typed, providing autocomplete and type checking.
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

const editor = new Editor({
  extensions: [StarterKit],
  content: '<p>Hello World</p>',
})

// TypeScript knows all editor properties and methods
editor.commands.setContent('<p>New content</p>')
editor.isEditable // boolean
editor.state // EditorState
editor.view // EditorView

Extension Options

Define typed options for your extensions.
import { Extension } from '@tiptap/core'

export interface MyExtensionOptions {
  /**
   * The prefix to use
   * @default 'default'
   */
  prefix: string
  /**
   * Maximum length
   * @default 100
   */
  maxLength: number
  /**
   * Enable feature
   * @default false
   */
  enabled: boolean
}

export const MyExtension = Extension.create<MyExtensionOptions>({
  name: 'myExtension',
  
  addOptions() {
    return {
      prefix: 'default',
      maxLength: 100,
      enabled: false,
    }
  },
  
  addCommands() {
    return {
      setPrefix: (prefix: string) => ({ commands }) => {
        // TypeScript knows this.options.prefix exists
        console.log(this.options.prefix)
        return true
      },
    }
  },
})

// Usage with type checking
const editor = new Editor({
  extensions: [
    MyExtension.configure({
      prefix: 'custom', // ✓ Type checked
      maxLength: 200,   // ✓ Type checked
      // invalid: true, // ✗ TypeScript error
    }),
  ],
})
Source: packages/core/src/Extendable.ts:57

Node Options and Attributes

Type your node options and attributes.
import { Node, mergeAttributes } from '@tiptap/core'
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'

export type Level = 1 | 2 | 3 | 4 | 5 | 6

export interface HeadingOptions {
  levels: Level[]
  HTMLAttributes: Record<string, any>
}

export const Heading = Node.create<HeadingOptions>({
  name: 'heading',
  
  addOptions() {
    return {
      levels: [1, 2, 3, 4, 5, 6],
      HTMLAttributes: {},
    }
  },
  
  addAttributes() {
    return {
      level: {
        default: 1,
        rendered: false,
      },
    }
  },
  
  renderHTML({ node, HTMLAttributes }) {
    // TypeScript knows node.attrs.level is a number
    const level: Level = node.attrs.level
    const hasLevel = this.options.levels.includes(level)
    
    return [
      `h${hasLevel ? level : this.options.levels[0]}`,
      mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
      0,
    ]
  },
})
Source: packages/extension-heading/src/heading.ts:6

Mark Options and Attributes

Type your mark options and attributes.
import { Mark, mergeAttributes } from '@tiptap/core'
import type { Mark as ProseMirrorMark } from '@tiptap/pm/model'

export interface HighlightOptions {
  multicolor: boolean
  HTMLAttributes: Record<string, any>
}

export const Highlight = Mark.create<HighlightOptions>({
  name: 'highlight',
  
  addOptions() {
    return {
      multicolor: false,
      HTMLAttributes: {},
    }
  },
  
  addAttributes() {
    if (!this.options.multicolor) {
      return {}
    }
    
    return {
      color: {
        default: null,
        parseHTML: element => 
          element.getAttribute('data-color') || element.style.backgroundColor,
        renderHTML: attributes => {
          if (!attributes.color) {
            return {}
          }
          return {
            'data-color': attributes.color,
            style: `background-color: ${attributes.color}`,
          }
        },
      },
    }
  },
})
Source: packages/extension-highlight/src/highlight.ts:3

Command Types

Extend the Commands interface for type-safe commands.
import { Extension } from '@tiptap/core'

// Extend the Commands interface
declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    myExtension: {
      /**
       * Insert a custom element
       * @example editor.commands.insertCustomElement('foo')
       */
      insertCustomElement: (id: string) => ReturnType
      /**
       * Set custom value
       * @example editor.commands.setCustomValue(42)
       */
      setCustomValue: (value: number) => ReturnType
    }
  }
}

export const MyExtension = Extension.create({
  name: 'myExtension',
  
  addCommands() {
    return {
      insertCustomElement: (id: string) => ({ commands }) => {
        return commands.insertContent(`<div data-id="${id}"></div>`)
      },
      setCustomValue: (value: number) => ({ editor }) => {
        console.log(`Setting value to ${value}`)
        return true
      },
    }
  },
})

// Now TypeScript knows about these commands
const editor = new Editor({
  extensions: [MyExtension],
})

editor.commands.insertCustomElement('my-id') // ✓ Type safe
editor.commands.setCustomValue(42)           // ✓ Type safe
// editor.commands.unknownCommand()          // ✗ TypeScript error
Source: packages/extension-bold/src/bold.tsx:13

Storage Types

Type your extension storage.
import { Extension } from '@tiptap/core'

export interface MyExtensionStorage {
  count: number
  items: string[]
  metadata: Record<string, any>
}

// Extend the Storage interface
declare module '@tiptap/core' {
  interface Storage {
    myExtension: MyExtensionStorage
  }
}

export const MyExtension = Extension.create<{}, MyExtensionStorage>({
  name: 'myExtension',
  
  addStorage() {
    return {
      count: 0,
      items: [],
      metadata: {},
    }
  },
  
  addCommands() {
    return {
      incrementCount: () => ({ editor }) => {
        // TypeScript knows storage shape
        this.storage.count++
        return true
      },
      addItem: (item: string) => ({ editor }) => {
        this.storage.items.push(item)
        return true
      },
    }
  },
})

// Access storage with types
const count: number = editor.storage.myExtension.count
const items: string[] = editor.storage.myExtension.items
Source: packages/extension-collaboration/src/collaboration.ts:12

Editor Options Type

Type your editor configuration.
import { Editor, EditorOptions } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

const options: Partial<EditorOptions> = {
  element: document.querySelector('.editor') as HTMLElement,
  extensions: [StarterKit],
  content: '<p>Hello World</p>',
  editable: true,
  autofocus: 'end',
  injectCSS: true,
  
  onCreate: ({ editor }) => {
    console.log('Editor created', editor)
  },
  
  onUpdate: ({ editor, transaction }) => {
    console.log('Content updated', editor.getHTML())
  },
  
  onSelectionUpdate: ({ editor }) => {
    console.log('Selection changed')
  },
  
  onTransaction: ({ editor, transaction }) => {
    console.log('Transaction', transaction)
  },
  
  onFocus: ({ editor, event }) => {
    console.log('Editor focused')
  },
  
  onBlur: ({ editor, event }) => {
    console.log('Editor blurred')
  },
  
  onDestroy: () => {
    console.log('Editor destroyed')
  },
}

const editor = new Editor(options)
Source: packages/core/src/types.ts:286

Generic Types

Use generic types for flexible extensions.
import { Extension } from '@tiptap/core'

export interface ListOptions<T> {
  items: T[]
  getLabel: (item: T) => string
  getValue: (item: T) => string | number
}

export function createListExtension<T>() {
  return Extension.create<ListOptions<T>>({
    name: 'list',
    
    addOptions() {
      return {
        items: [],
        getLabel: (item: T) => String(item),
        getValue: (item: T) => String(item),
      }
    },
    
    addCommands() {
      return {
        selectItem: (item: T) => ({ editor }) => {
          const label = this.options.getLabel(item)
          const value = this.options.getValue(item)
          console.log(`Selected: ${label} (${value})`)
          return true
        },
      }
    },
  })
}

// Usage with specific type
interface User {
  id: number
  name: string
}

const UserList = createListExtension<User>()

const editor = new Editor({
  extensions: [
    UserList.configure({
      items: [
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ],
      getLabel: user => user.name,
      getValue: user => user.id,
    }),
  ],
})

React Hook Types

Type-safe React hooks with Tiptap.
import { useEditor, EditorContent } from '@tiptap/react'
import type { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { useCallback } from 'react'

export default function TypedEditor() {
  const editor: Editor | null = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World</p>',
  })
  
  // Type-safe command handler
  const setBold = useCallback(() => {
    editor?.chain().focus().toggleBold().run()
  }, [editor])
  
  // Type-safe state checks
  const isBold: boolean = editor?.isActive('bold') ?? false
  const isEditable: boolean = editor?.isEditable ?? true
  
  if (!editor) {
    return null
  }
  
  return (
    <div>
      <button 
        onClick={setBold}
        className={isBold ? 'active' : ''}
      >
        Bold
      </button>
      <EditorContent editor={editor} />
    </div>
  )
}

Utility Types

Tiptap provides useful utility types.
import type {
  Editor,
  Extensions,
  JSONContent,
  Content,
  Command,
  CommandProps,
  NodeViewRenderer,
  MarkViewRenderer,
} from '@tiptap/core'
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
import type { EditorState, Transaction } from '@tiptap/pm/state'

// JSON content type
const content: JSONContent = {
  type: 'doc',
  content: [
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: 'Hello World',
        },
      ],
    },
  ],
}

// Command type
const myCommand: Command = ({ editor, commands, state, tr }) => {
  return commands.insertContent('Hello')
}

// Extension array type
const extensions: Extensions = [StarterKit]

Type Guards

Use type guards for runtime type checking.
import type { Editor } from '@tiptap/core'
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'

// Check if node is specific type
function isHeading(node: ProseMirrorNode): boolean {
  return node.type.name === 'heading'
}

function isParagraph(node: ProseMirrorNode): boolean {
  return node.type.name === 'paragraph'
}

// Use in code
const node = editor.state.selection.$anchor.parent

if (isHeading(node)) {
  const level = node.attrs.level
  console.log(`Current heading level: ${level}`)
}

if (isParagraph(node)) {
  console.log('Currently in a paragraph')
}

Strict Mode

Enable strict TypeScript checking.
{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "resolveJsonModule": true
  }
}

Common Patterns

Useful TypeScript patterns for Tiptap.
import type { Editor } from '@tiptap/core'
import type { Node as ProseMirrorNode } from '@tiptap/pm/model'

// Optional chaining for commands
editor?.commands.toggleBold()

// Nullish coalescing for defaults
const isActive = editor?.isActive('bold') ?? false

// Type assertion when you know the type
const element = document.querySelector('.editor') as HTMLElement

// Non-null assertion (use sparingly)
const content = editor!.getHTML()

// Function overloads
function getContent(editor: Editor): string
function getContent(editor: Editor, format: 'html'): string
function getContent(editor: Editor, format: 'json'): JSONContent
function getContent(
  editor: Editor, 
  format: 'html' | 'json' = 'html'
): string | JSONContent {
  return format === 'json' ? editor.getJSON() : editor.getHTML()
}

// Discriminated unions
type Result = 
  | { success: true; data: string }
  | { success: false; error: string }

function handleResult(result: Result) {
  if (result.success) {
    console.log(result.data)  // TypeScript knows this exists
  } else {
    console.error(result.error)  // TypeScript knows this exists
  }
}
Tiptap’s TypeScript support helps catch errors at compile time and provides excellent autocomplete in your IDE.

Next Steps

Custom Extensions

Create type-safe custom extensions

Custom Nodes

Build type-safe custom nodes

Build docs developers (and LLMs) love