Are TypeScript types wrong or are we all doing something wrong?
I've been using parse5 for months with TypeScript and I always had to fight with the typings, assuming I was doing something wrong but unable to understand what. Today I took the time to test the code samples that are shipped with the typings, for example:
https://github.com/inikulin/parse5/blob/0abd4406ae5b518a06b5df3d61bc332fc51887c1/packages/parse5/lib/serializer/index.ts#L56
And the code sample itself doesn't satisfy the typechecker:
import * as parse5 from 'parse5';
const document = parse5.parse('<!DOCTYPE html><html><head></head><body>Hi there!</body></html>');
// Serializes a document.
const html = parse5.serialize(document);
// Serializes the <html> element content.
const str = parse5.serialize(document.childNodes[1]); // TS2345: Argument of type ChildNode is not assignable to parameter of type ParentNode
console.log(str); //> '<head></head><body>Hi there!</body>'
I feel reassured - I may not be doing anything wrong - and concerned at the same time: parse5 is written in TypeScript. I'm not sure I understand how typings could be wrong if the code can be tested and built.
Note that the above code works perfectly fine once compiled into JavaScript.
the childNodes will be of type ChildNode, but not all child nodes are parent nodes (e.g. a text node can be a child but not a parent).
the type system can't know what is in your HTML string so its upto you to narrow that union.
usually you'd do this with the type guards of the tree adapter, for example:
const node = document.childNodes[1];
if (parse5.defaultTreeAdapter.isElementNode(node)) {
// node is now a ParentNode
}
or because your HTML is static, just cast it to parse5.DefaultTreeAdapterTypeMap['element']
My point is why is the seralize method only able to serialize a ParentNode - or, more acurately, why is it declared as such - even though it can serialize an Element without any issue?
Actually, it can serialize something as soon as it implements a very narrow subset of what an Element is. For example, the following code works:
serialize({
tagName: 'foo'
});
And of course, this works too:
serialize({
tagName: 'foo',
childNodes: [
{
tagName: 'bar',
attrs: [{
name: 'data-foo',
value: '5'
}]
}
]
}); // <bar data-foo="5"></bar>
Shouldn't the types expose that fact? What is the point of exposing types that are more restrictive than the actual implementation? If tagName is the only required property, and everything else is optional, then why is the type not reflecting this?
an Element is a ParentNode
its a union, like Element | Document | ...
just the same, Node is a union like Element | TextNode | Document | ...
you can't currently serialize a text node, the serializer expects the root node to be a parent node (element, document, etc)
the types reflect this by defining that you can only serialize nodes which can have children (ParentNode)