-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Avoid overly-cautious type guards in favor of more performant type discriminators #5982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…sing them whenever possible. also switch to `node === editor` checks where it makes sense also add a comment to a confusing failure state of `Node.parent` also remove a superfluous call to `Editor.nodes` in certain codepaths of `Editor.wrapNodes`
🦋 Changeset detectedLatest commit: 2f75743 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| return ( | ||
| node !== editor && | ||
| (Text.isTextNode(node) || | ||
| Editor.isVoid(editor, node) || | ||
| (node.children.length === 1 && | ||
| hasSingleChildNest(editor, node.children[0]))) | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same functionality, rearranged for readability.
Basically its a nest if its a leaf or its only child is a leaf--and editor doesn't count.
|
|
||
| const roots = Array.from( | ||
| Editor.nodes(editor, { | ||
| at, | ||
| match: editor.isInline(element) | ||
| ? n => Element.isElement(n) && Editor.isBlock(editor, n) | ||
| : n => Editor.isEditor(n), | ||
| mode: 'lowest', | ||
| voids, | ||
| }) | ||
| ) | ||
| const roots = editor.isInline(element) | ||
| ? Editor.nodes(editor, { | ||
| at, | ||
| match: n => Element.isElementNode(n) && Editor.isBlock(editor, n), | ||
| mode: 'lowest', | ||
| voids, | ||
| }) | ||
| : [[editor, []] as NodeEntry] | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Editor.nodes with match: n => Editor.isEditor(n) only ever returns the same one node: the editor.
| }, | ||
|
|
||
| extractProps(node: Node): NodeProps { | ||
| if (Element.isAncestor(node)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be worth adding an Element.isAncestorNode to make it easier to convert existing code like this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we did it would probably end up as just a wrapper around !Text.isTextNode (I'd assume checking string typing is faster than checking array typing although tbf I haven't checked)
I'm not personally a fan of those sorts of inverse functions-- "is not a leaf" p directly translates to "has children" in my mind. That being said it doesn't really get in my way and I don't hate it enough to boycott it if it were added.
|
|
||
| Transforms.liftNodes(editor, { | ||
| at: range, | ||
| match: n => Element.isAncestor(node) && node.children.includes(n), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. I find isAncestor more expressive, as it essentially means "has children".
| Add `Editor.isEditorNode`, `Element.isElementNode`, and `Text.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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Should we also change the docs to refer to the new type guards by default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I definitely think so, user code should only need the old functions for serializing/deserializing etc, basically call match functions etc are better off with the new ones.
Just didn't want to cement them in too deep before getting feedback on the naming. Thankfully old code still functions, just inefficiently (equally as inefficiently as before, to be precise)
| return false | ||
| } | ||
|
|
||
| const isEditor = |
There was a problem hiding this comment.
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 |
|---|---|---|
![]() |
![]() |
![]() |
There was a problem hiding this comment.
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. 🤔
The other packages don't use the live versions; I believe they use the transpiled versions of the packages found in Bear in mind that if you do update the other packages to depend on the new functions, you'll also need to bump the peer dependency versions. |
I would be in favour of this personally, but I'd be interested to hear what others think. |
Well that'll show me assuming repos other people create are as jank as my own. In that case I can add the other packages changes to this PR |
| onMouseDown={event => { | ||
| event.preventDefault() | ||
| onPointerDown={event => event.preventDefault()} | ||
| onClick={() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these were changed by yarn tsc:examples, they already existed in the tsx files and weren't synced
|
replaced the calls in other packages as well. A few were left in intentionally: https://github.com/nabbydude/slate/blob/2f75743c89b7561d95b1b5cd8cd5a88f7f5dc925/packages/slate-react/src/components/slate.tsx#L47-L51 https://github.com/nabbydude/slate/blob/2f75743c89b7561d95b1b5cd8cd5a88f7f5dc925/packages/slate-hyperscript/src/creators.ts#L35 I also didn't touch any of the tests, since rigorous checks have purpose there. |



Description
Adds
Editor.isEditorNode,Element.isElementNode, andText.isTextNodethat take aNodeargument and check quickly if it's a specific node type.Also fixes classic
isEditorto actually check for all properties, actually respect deepness settings (a bug I only noticed because I was editing the file 😅), and also respect those deepness settings for its operation list.This also removes any calls to
Element.isAncestor, since it was only ever used where!Text.isTextNodewould function just fine. It doesn't remove the function itself since its user accessible. Rigourously testing for any of two node types fromanylike this is something I cant think of a real use case for and it might be worth deprecating.Issue
Fixes #5980
Basically checking if something is an object all the time and checking it has every single function its supposed to is a waste of performance, 99% of the time we can just quickly check if its definitely not one of the other two types and move on.
Context
This touches a lot of files, since it replaces almost every call of
isEditor,isElement, andisTextin theslatepackage.I've removed a few redundant calls and cleaned up a few spaghetti boolean expressions.
This only changes the
slatepackage (mostly because the packages depend on the live versions of eachother and typescript complained if I tried to call the newly added methods) the other packages can be changed once this is merged, and until then will continue to operate gracefully. Same for any user code.Editor.isEditorpreviously only passed its first argument to the function it wrapped, which meant deepness options were ignored. this PR just adds that function directly to the object without wrapping it to avoid that happening again.isEditorNodeis included in the same way.I have another open PR (#5970) that changes
normalizeNode, which this will conflict with. If both end up approved I have a merging commit ready.Alternatives
I chose these names mostly to be close to the originals in alphabetical order, and to keep the
isXparadigm. other options could benodeIsEditor,isNodeEditor,Node.isEditor. I can change them if something else is preferred.We could overwrite the old functionality and migrate the cautious type guard to
verifyElementor some other name but that might break existing user code.Future
Checks
yarn test.yarn lint. (Fix errors withyarn fix.)yarn start.)yarn changeset add.)