WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
6 changes: 6 additions & 0 deletions .changeset/rich-cheetahs-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'slate': minor
---
Add `isEditorNode`, `isElementNode`, and `isTextNode` as alternative type guards for when we already know the object is a node.
Use these new functions instead of `isEditor`, `isElement`, and `isText` whenever possible, the classic functions are only necessary for typechecking an entirely unknown object.
===
2 changes: 1 addition & 1 deletion packages/slate/src/core/get-dirty-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const getDirtyPaths: WithEditorFirstArg<Editor['getDirtyPaths']> = (
case 'insert_node': {
const { node, path } = op
const levels = Path.levels(path)
const descendants = Text.isText(node)
const descendants = Text.isTextNode(node)
? []
: Array.from(Node.nodes(node), ([, p]) => path.concat(p))

Expand Down
32 changes: 17 additions & 15 deletions packages/slate/src/core/normalize-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
const [node, path] = entry

// There are no core normalizations for text nodes.
if (Text.isText(node)) {
if (Text.isTextNode(node)) {
return
}

// Ensure that block and inline nodes have at least one text child.
if (Element.isElement(node) && node.children.length === 0) {
if (Element.isElementNode(node) && node.children.length === 0) {
const child = { text: '' }
Transforms.insertNodes(editor, child, {
at: path.concat(0),
Expand All @@ -28,26 +28,28 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
}

// Determine whether the node should have block or inline children.
const shouldHaveInlines = Editor.isEditor(node)
? false
: Element.isElement(node) &&
(editor.isInline(node) ||
node.children.length === 0 ||
Text.isText(node.children[0]) ||
editor.isInline(node.children[0]))
const shouldHaveInlines =
node === editor
? false
: Element.isElementNode(node) &&
(editor.isInline(node) ||
node.children.length === 0 ||
Text.isTextNode(node.children[0]) ||
editor.isInline(node.children[0]))

// Since we'll be applying operations while iterating, keep track of an
// index that accounts for any added/removed nodes.
let n = 0

for (let i = 0; i < node.children.length; i++, n++) {
const currentNode = Node.get(editor, path)
if (Text.isText(currentNode)) continue
if (Text.isTextNode(currentNode)) continue
const child = currentNode.children[n] as Descendant
const prev = currentNode.children[n - 1] as Descendant
const isLast = i === node.children.length - 1
const isInlineOrText =
Text.isText(child) || (Element.isElement(child) && editor.isInline(child))
Text.isTextNode(child) ||
(Element.isElementNode(child) && editor.isInline(child))

// Only allow block nodes in the top-level children and parent blocks
// that only contain block nodes. Similarly, only allow inline nodes in
Expand All @@ -67,10 +69,10 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
Transforms.unwrapNodes(editor, { at: path.concat(n), voids: true })
}
n--
} else if (Element.isElement(child)) {
} else if (Element.isElementNode(child)) {
// Ensure that inline nodes are surrounded by text nodes.
if (editor.isInline(child)) {
if (prev == null || !Text.isText(prev)) {
if (prev == null || !Text.isTextNode(prev)) {
const newChild = { text: '' }
Transforms.insertNodes(editor, newChild, {
at: path.concat(n),
Expand All @@ -95,13 +97,13 @@ export const normalizeNode: WithEditorFirstArg<Editor['normalizeNode']> = (
// To prevent slate from breaking, we can add the `children` field,
// and now that it is valid, we can to many more operations easily,
// such as extend normalizers to fix erronous structure.
if (!Text.isText(child) && !('children' in child)) {
if (!Text.isTextNode(child) && !('children' in child)) {
const elementChild = child as Element
elementChild.children = []
}

// Merge adjacent text nodes that are empty or match.
if (prev != null && Text.isText(prev)) {
if (prev != null && Text.isTextNode(prev)) {
if (Text.equals(child, prev, { loose: true })) {
Transforms.mergeNodes(editor, { at: path.concat(n), voids: true })
n--
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/add-mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const addMark: EditorInterface['addMark'] = (editor, key, value) => {

if (selection) {
const match = (node: Node, path: Path) => {
if (!Text.isText(node)) {
if (!Text.isTextNode(node)) {
return false // marks can only be applied to text
}
const [parentNode, parentPath] = Editor.parent(editor, path)
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/element-read-only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export const elementReadOnly: EditorInterface['elementReadOnly'] = (
) => {
return Editor.above(editor, {
...options,
match: n => Element.isElement(n) && Editor.isElementReadOnly(editor, n),
match: n => Element.isElementNode(n) && Editor.isElementReadOnly(editor, n),
})
}
2 changes: 1 addition & 1 deletion packages/slate/src/editor/get-void.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { Element } from '../interfaces/element'
export const getVoid: EditorInterface['void'] = (editor, options = {}) => {
return Editor.above(editor, {
...options,
match: n => Element.isElement(n) && Editor.isVoid(editor, n),
match: n => Element.isElementNode(n) && Editor.isVoid(editor, n),
})
}
2 changes: 1 addition & 1 deletion packages/slate/src/editor/has-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { Element } from '../interfaces/element'

export const hasBlocks: EditorInterface['hasBlocks'] = (editor, element) => {
return element.children.some(
n => Element.isElement(n) && Editor.isBlock(editor, n)
n => Element.isElementNode(n) && Editor.isBlock(editor, n)
)
}
2 changes: 1 addition & 1 deletion packages/slate/src/editor/has-inlines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { Text } from '../interfaces/text'

export const hasInlines: EditorInterface['hasInlines'] = (editor, element) => {
return element.children.some(
n => Text.isText(n) || Editor.isInline(editor, n)
n => Text.isTextNode(n) || Editor.isInline(editor, n)
)
}
2 changes: 1 addition & 1 deletion packages/slate/src/editor/has-texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { EditorInterface } from '../interfaces/editor'
import { Text } from '../interfaces/text'

export const hasTexts: EditorInterface['hasTexts'] = (editor, element) => {
return element.children.every(n => Text.isText(n))
return element.children.every(n => Text.isTextNode(n))
}
7 changes: 7 additions & 0 deletions packages/slate/src/editor/is-editor-node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Editor, EditorInterface, Node } from '../interfaces'

export const isEditorNode: EditorInterface['isEditorNode'] = (
node: Node
): node is Editor => {
return typeof (node as Editor).apply === 'function'
}
77 changes: 73 additions & 4 deletions packages/slate/src/editor/is-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,95 @@ export const isEditor: EditorInterface['isEditor'] = (
}

const isEditor =
Copy link
Contributor

@12joan 12joan Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether it's worth including this many typeof value.x === 'function' checks. Each one carries a small overhead, and there's a big difference between having 16 of them versus 86 (apologies if I miscounted).

Here's a benchmark I ran to compare these numbers of typeof checks. Since the numbers don't mean much in isolation, I also included a WeakMap.prototype.set call for comparison. Setting and getting weak map values tends to be relatively slow, so anything slower than it should generally be avoided if possible.

https://jsbm.dev/xM6mIrc116gyW - Longer bars are faster.

Firefox Chrome Safari
image image image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my intention would be that Editor.isEditor is basically never used. The only current use case I can imagine in the current codebase is maybe the sanity check that slate-react's Slate component does when its first passed an editor prop but really that should be unnecessary in a typescripted world.

As I'm typing though I do see the perspective of any user code that updates its dependency without tweaking its calls getting an unexpected performance hit. 🤔

typeof value.above === 'function' &&
typeof value.addMark === 'function' &&
typeof value.after === 'function' &&
typeof value.apply === 'function' &&
typeof value.before === 'function' &&
typeof value.collapse === 'function' &&
typeof value.delete === 'function' &&
typeof value.deleteBackward === 'function' &&
typeof value.deleteForward === 'function' &&
typeof value.deleteFragment === 'function' &&
typeof value.deselect === 'function' &&
typeof value.edges === 'function' &&
typeof value.elementReadOnly === 'function' &&
typeof value.end === 'function' &&
typeof value.first === 'function' &&
typeof value.fragment === 'function' &&
typeof value.getDirtyPaths === 'function' &&
typeof value.getFragment === 'function' &&
typeof value.getMarks === 'function' &&
typeof value.hasBlocks === 'function' &&
typeof value.hasInlines === 'function' &&
typeof value.hasPath === 'function' &&
typeof value.hasTexts === 'function' &&
typeof value.insertBreak === 'function' &&
typeof value.insertSoftBreak === 'function' &&
typeof value.insertFragment === 'function' &&
typeof value.insertNode === 'function' &&
typeof value.insertNodes === 'function' &&
typeof value.insertSoftBreak === 'function' &&
typeof value.insertText === 'function' &&
typeof value.isBlock === 'function' &&
typeof value.isEdge === 'function' &&
typeof value.isElementReadOnly === 'function' &&
typeof value.isEmpty === 'function' &&
typeof value.isEnd === 'function' &&
typeof value.isInline === 'function' &&
typeof value.isNormalizing === 'function' &&
typeof value.isSelectable === 'function' &&
typeof value.isStart === 'function' &&
typeof value.isVoid === 'function' &&
typeof value.last === 'function' &&
typeof value.leaf === 'function' &&
typeof value.levels === 'function' &&
typeof value.liftNodes === 'function' &&
typeof value.markableVoid === 'function' &&
typeof value.mergeNodes === 'function' &&
typeof value.move === 'function' &&
typeof value.moveNodes === 'function' &&
typeof value.next === 'function' &&
typeof value.node === 'function' &&
typeof value.nodes === 'function' &&
typeof value.normalize === 'function' &&
typeof value.normalizeNode === 'function' &&
typeof value.onChange === 'function' &&
typeof value.parent === 'function' &&
typeof value.path === 'function' &&
typeof value.pathRef === 'function' &&
typeof value.pathRefs === 'function' &&
typeof value.point === 'function' &&
typeof value.pointRef === 'function' &&
typeof value.pointRefs === 'function' &&
typeof value.positions === 'function' &&
typeof value.previous === 'function' &&
typeof value.range === 'function' &&
typeof value.rangeRef === 'function' &&
typeof value.rangeRefs === 'function' &&
typeof value.removeMark === 'function' &&
typeof value.getDirtyPaths === 'function' &&
typeof value.removeNodes === 'function' &&
typeof value.select === 'function' &&
typeof value.setNodes === 'function' &&
typeof value.setNormalizing === 'function' &&
typeof value.setPoint === 'function' &&
typeof value.setSelection === 'function' &&
typeof value.shouldMergeNodesRemovePrevNode === 'function' &&
typeof value.shouldNormalize === 'function' &&
typeof value.splitNodes === 'function' &&
typeof value.start === 'function' &&
typeof value.string === 'function' &&
typeof value.unhangRange === 'function' &&
typeof value.unsetNodes === 'function' &&
typeof value.unwrapNodes === 'function' &&
typeof value.void === 'function' &&
typeof value.withoutNormalizing === 'function' &&
typeof value.wrapNodes === 'function' &&
(value.marks === null || isObject(value.marks)) &&
(value.selection === null || Range.isRange(value.selection)) &&
(!deep || Node.isNodeList(value.children)) &&
Operation.isOperationList(value.operations)
(deep
? Node.isNodeList(value.children) &&
Operation.isOperationList(value.operations)
: Array.isArray(value.children) && Array.isArray(value.operations))

return isEditor
}
2 changes: 1 addition & 1 deletion packages/slate/src/editor/is-empty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const isEmpty: EditorInterface['isEmpty'] = (editor, element) => {
return (
children.length === 0 ||
(children.length === 1 &&
Text.isText(first) &&
Text.isTextNode(first) &&
first.text === '' &&
!editor.isVoid(element))
)
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/levels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function* levels<T extends Node>(

levels.push([n, p] as NodeEntry<T>)

if (!voids && Element.isElement(n) && Editor.isVoid(editor, n)) {
if (!voids && Element.isElementNode(n) && Editor.isVoid(editor, n)) {
break
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/slate/src/editor/marks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ export const marks: EditorInterface['marks'] = (editor, options = {}) => {
const prev = Editor.previous(editor, { at: path, match: Text.isText })
const markedVoid = Editor.above(editor, {
match: n =>
Element.isElement(n) &&
Element.isElementNode(n) &&
Editor.isVoid(editor, n) &&
editor.markableVoid(n),
})
if (!markedVoid) {
const block = Editor.above(editor, {
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
match: n => Element.isElementNode(n) && Editor.isBlock(editor, n),
})

if (prev && block) {
Expand Down
4 changes: 2 additions & 2 deletions packages/slate/src/editor/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function* nodes<T extends Node>(
to,
pass: ([node, path]) => {
if (pass && pass([node, path])) return true
if (!Element.isElement(node)) return false
if (!Element.isElementNode(node)) return false
if (
!voids &&
(Editor.isVoid(editor, node) || Editor.isElementReadOnly(editor, node))
Expand All @@ -72,7 +72,7 @@ export function* nodes<T extends Node>(
// If we've arrived at a leaf text node that is not lower than the last
// hit, then we've found a branch that doesn't include a match, which
// means the match is not universal.
if (universal && !isLower && Text.isText(node)) {
if (universal && !isLower && Text.isTextNode(node)) {
return
} else {
continue
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const normalize: EditorInterface['normalize'] = (
As long as the normalizer only inserts child nodes for this case it is safe to do in any order;
by definition adding children to an empty node can't cause other paths to change.
*/
if (Element.isElement(node) && node.children.length === 0) {
if (Element.isElementNode(node) && node.children.length === 0) {
editor.normalizeNode(entry, { operation })
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const point: EditorInterface['point'] = (editor, at, options = {}) => {

const node = Node.get(editor, path)

if (!Text.isText(node)) {
if (!Text.isTextNode(node)) {
throw new Error(
`Cannot get the ${edge} point in the node at path [${at}] because it has no ${edge} text node.`
)
Expand Down
4 changes: 2 additions & 2 deletions packages/slate/src/editor/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function* positions(
/*
* ELEMENT NODE - Yield position(s) for voids, collect blockText for blocks
*/
if (Element.isElement(node)) {
if (Element.isElementNode(node)) {
if (!editor.isSelectable(node)) {
/**
* If the node is not selectable, skip it and its descendants
Expand Down Expand Up @@ -139,7 +139,7 @@ export function* positions(
* TEXT LEAF NODE - Iterate through text content, yielding
* positions every `distance` offset according to `unit`.
*/
if (Text.isText(node)) {
if (Text.isTextNode(node)) {
const isFirst = Path.equals(path, first.path)

// Proof that we always exhaust text nodes before encountering a new one:
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/remove-mark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const removeMark: EditorInterface['removeMark'] = (editor, key) => {

if (selection) {
const match = (node: Node, path: Path) => {
if (!Text.isText(node)) {
if (!Text.isTextNode(node)) {
return false // marks can only be applied to text
}
const [parentNode, parentPath] = Editor.parent(editor, path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export const shouldMergeNodesRemovePrevNode: EditorInterface['shouldMergeNodesRe
// if prevNode is first child in parent,don't remove it.

return (
(Element.isElement(prevNode) && Editor.isEmpty(editor, prevNode)) ||
(Text.isText(prevNode) &&
(Element.isElementNode(prevNode) && Editor.isEmpty(editor, prevNode)) ||
(Text.isTextNode(prevNode) &&
prevNode.text === '' &&
prevPath[prevPath.length - 1] !== 0)
)
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/editor/unhang-range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const unhangRange: EditorInterface['unhangRange'] = (

const endBlock = Editor.above(editor, {
at: end,
match: n => Element.isElement(n) && Editor.isBlock(editor, n),
match: n => Element.isElementNode(n) && Editor.isBlock(editor, n),
voids,
})
const blockPath = endBlock ? endBlock[1] : []
Expand Down
Loading
Loading