Fonto Why & How: Why is my useXPath giving TypeScript errors!?

Fonto Why & How: Why is my useXPath giving TypeScript errors!?

In this weekly series, Martin describes a question that was raised by a Fonto developer, how it was resolved and why Fonto behaved like that in the first place. This week, a partner is running into TypeScript errors when calling evaluateXPath and useXPath!

A support issue came in from a partner who’s following the metadata sidebar guide:

Hey there!

We’re following the metadata sidebar guide and we’re experiencing some TypeScript warnings and errors. We assume the guide is just a bit outdated because it seems to be written in JSX, missing any TypeScript type annotations. Can you help us with that?

A Fonto Partner

Recently in Fonto 8.0 we introduced TypeScript. We typed almost all of our APIs and we are continuing to do so for the rest. Some of our APIs are easier to type than others, like the getNodeId function, which accepts a FontoNode and returns a NodeId. Some others are harder to type, like our useXPath React Hook, which can return a string, a number, a boolean or (an array of) nodes, depending on the ReturnType it is passed.

We are still working on porting all the code examples in the documentation environment, so we highly appreciate it when you point it out to us when an example is outdated. We will give it more priority to fix!

In this guide, we’re missing some type arguments that cause various type issues. Let’s go over them:

const metadataTextContent = useXPath('string(.)', metadataElement, {
    expectedResultType: ReturnTypes.STRING,
});

// ...

<TextInput value={metadataTextContent} />
// Type 'XQConvertibleValue<"immutable">' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.ts(2322)

This error is trying to say the type of metadataTextContent is not always a string. Which is right: without providing no type argument to useXPath, it defaults to the type XQConvertibleValue. Which expands to anything an XPath can resolve to. This includes strings, numbers, nodes, booleans, and more.

By passing a type argument, the error disappears:

const metadataTextContent = useXPath<string>('string(.)', metadataElement, {
    expectedResultType: ReturnTypes.STRING,
});

This error is gone. Up to the next one:

const metadataElement = useXPath('document/metadata', focusedDocumentNode, {
    expectedResultType: ReturnTypes.FIRST_NODE,
});

const nodeId = useMemo(() => getNodeId(metadataElement), [metadataElement])
// "argument of type 'XQConvertibleValue<"immutable">' is not assignable to parameter of type '{ readonly ELEMENT_NODE: number; readonly ATTRIBUTE_NODE: number; readonly TEXT_NODE: number; readonly CDATA_SECTION_NODE: number; readonly ENTITY_REFERENCE_NODE: number; readonly ENTITY_NODE: number; ... 7 more ...; readonly nodeName: string; }'."

Essentially, this is the same error: An XQConvertibleValue (which includes a single node, an array of nodes, strings and more) is passed to a function that only accepts a FontoNode. Even though the code always makes this work, TypeScript needs to be told that.

The following code makes the error clear up:

const metadataElement = useXPath<FontoNode>('document/metadata', focusedDocumentNode, {
    expectedResultType: ReturnTypes.FIRST_NODE,
});

This was the last error in this example. We updated the example, and we’re done!

But why?

But why did TypeScript not know what the return type was? We passed an argument that indicates the type, right?

That is exactly what our developers asked themselves. And we fixed it! By adding overloads to all XPath APIs, we can solve this. We basically rewrote the the useXPath signature to read like this, and all of the fixes above were not needed anymore:

function useXPath(
	expression: XPathQuery | XPathTest | XQExpression | null,
	context: FontoNode | null,
	options: XPathObserverOptions & { expectedResultType: ReturnTypes.BOOLEAN }
): boolean;
function useXPath(
	expression: XPathQuery | XPathTest | XQExpression | null,
	context: FontoNode | null,
	options: XPathObserverOptions & { expectedResultType: ReturnTypes.NODES }
): FontoNode[];
// Same for all the other types and overloads

This will be included in the upcoming Fonto release, which is 8.3. I hope this blogpost still gives you insight in our way of working and addressing bugs! If you spot any mistakes in our documentation, always reach out! Your feedback helps us!

I hope this explained how Fonto works and why it works like that. During the years we built quite the product, and we are aware some parts work in unexpected ways for those who have not been with it from the start. If you have any points of Fonto you would like some focus on, we are always ready and willing to share! Reach out on Twitter to Martin Middel or file a support issue!

Stay up-to-dateFonto Why & How posts direct in your inbox

Receive updates on new Fonto Why & How blog posts by email

Scroll to Top