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
}),
],
})
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,
]
},
})
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}`,
}
},
},
}
},
})
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
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
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)
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