ParseRule
currently has the following type:
interface ParseRule {
/// A single rule should have _either_ a `tag` or a `style` property.
tag?: string
style?: string
/// Called with a DOM Element for `tag` rules, and with a string (the
/// style's value) for `style` rules.
getAttrs?: (node: HTMLElement | string) => Attrs | false | null
// ... other fields
}
Now if I have a ParseRule
like
const rule: ParseRule = {
tag: 'div',
getAttrs(node: HTMLElement) { // type error
return { class: node.className }
},
// ... other fields
}
which is perfectly fine at runtime, getAttrs
gets incorrectly marked as a type error by TypeScript compiler.
TypeScript compiler is happy with the much more verbose version:
const rule: ParseRule = {
tag: 'div',
getAttrs(node /* inferred as (HTMLElement | string) */) {
if (typeof node === 'string') throw new Error('IMPOSSIBLE')
return { class: node.className }
},
// ... other fields
}
But writing something like this every time is cumbersome.
ProseMirror/prosemirror-schema-basic bypasses this problem by casting an object blindly into NodeSpec
or MarkSpec
, which works because
(node: HTMLElement) => Attrs | false | null
is a superset of
(node: HTMLElement | string) => Attrs | false | null
(not the other way around!)
I think it is not right to force users to write needlessly verbose code or fallback to type assertions to work around the types. Two solutions come to my mind:
Make ParseRule
a union type
This makes the dependency of getAttrs
on whether tag
or style
is present clear. Among other benefits, it also ensures that one and only one of tag
or style
exists on the type level.
type ParseRule = (
| {
tag: string
namespace?: string
getAttrs: (node: HTMLElement) => Attrs | false | null
}
| {
style: string
getAttrs: (node: string) => Attrs | false | null
}
) & {
priority?: number
// ... other fields
}
The downside is that the type looks complex. Converting an interface to a type alias also means that other code cannot extend the interface with additional fields anymore, making it technically a breaking change in types, but I don’t think anybody should be augmenting ParseRule
anyway.
Tweak the type a bit for ease of use
Since a getAttrs
function should accept either HTMLElement
or string
but not both, it is reasonable to retype getAttrs
as
getAttrs?: ((node: HTMLElement) => Attrs | false | null)
| ((node: string) => Attrs | false | null)
This does not prevent errors like expecting an element with style
rules, but is a smaller and more compatible change to the current interface type. prosemirror-model needs to be modified accordingly to cast getAttrs
wherever it is called; the cast is unavoidable, because it is impossible to differentiate function types at runtime. Fortunately, there are only two call sites in matchTag
and matchStyle
.
I’d love to contribute a PR to fix the problem, but I am not sure about which solution would work better.