LINQ to XML for TypeScript: Why I Built It, and How Claude Helped
XML transformation in JavaScript has meant wrestling with DOMDocument. There is a better way — and it now exists for TypeScript.
I am pleased to announce LtXmlTs — a TypeScript implementation of LINQ to XML, available now on GitHub at github.com/EricWhiteDev/LtXmlTs under the MIT license.
Here is why I built it, and why it took the form it did.
The case for JavaScript Word add-ins
For anyone targeting macOS or Word Online, JavaScript add-ins are the only option. And unlike VSTO add-ins — which require cumbersome deployment cycles — a JavaScript add-in can be updated on the fly. For certain use cases, there is no other path.
Very often, the only way to implement real functionality in such an add-in is through direct manipulation of Open XML markup. Some developers sidestep this by farming the work out to a .NET or Python service. I think there are cleaner approaches — but they require doing XML transformation in TypeScript. And that means confronting the available tools.
The problem with DOMDocument
DOMDocument works. But revisiting it months later involves matching variable names scattered across the page and deducing intent from imperative steps. LINQ to XML is better because the structure of your code directly mirrors the XML you are producing — the nesting is the documentation. Recursive pure functional transformations are also the only clean way to implement certain classes of Open XML functionality, short of XSLT (which I dislike) or sprawling DOMDocument code.
| Criterion | DOMDocument | LINQ to XML |
| Code readability | Variable names must be traced up and down the page | Nesting hierarchy mirrors the XML being produced |
| Functional construction | Imperative; mutable nodes built step by step | Functional construction is declarative and easy to reason about |
| Recursive transforms | Difficult to express cleanly; code sprawls | Pure functional transformations are a first-class pattern |
| Long-term maintenance | Intent is buried; requires active reconstruction | Original intent is clear and on the surface |
A look at the difference
Consider a paragraph with formatted text — something that appears constantly in Open XML documents:
const paragraph = new XElement(W.p,
new XElement(W.pPr,
new XElement(W.pStyle,
new XAttribute(W.val, "Heading1")
)
),
new XElement(W.r,
new XElement(W.rPr,
new XElement(W.b)
),
new XElement(W.t,
new XAttribute(XML.space, "preserve"),
"Hello, Open XML"
)
)
);
The indentation is the documentation. No variable names to trace, no appendChild calls to sequence. The code is the structure.
Why I was the right person to build this
Years ago, as a Microsoft employee, I worked on the LINQ to XML team. I was required to know every detail of the library’s semantics — every edge case, every design decision. I later built a JavaScript implementation that shipped as part of the Open-Xml-Sdk-JavaScript and used it for years. But JavaScript is not TypeScript, and other TypeScript implementations I evaluated left me uncertain about their semantic fidelity. So I built my own.
How Claude Code made this possible
I used Claude Code throughout, but the process was not casual “vibing.” It was over one hundred targeted prompts — one class, one area of functionality at a time — with unit tests written and reviewed at each step. Intense, focused engineering using Claude as a powerful tool, not a magic wand.
Vibing? Not so much. Intense, focused engineering using Claude as a powerful tool — that is more accurate.
My knowledge of the LINQ to XML semantics was essential for verification. What Claude provided was velocity: implementations fleshed out rapidly, comprehensive test suites generated, iteration without writing every character by hand. The result was faster than manual development — and the code quality was higher.
The next phase is generating the Open XML manipulation code that uses this library. The engineering approach will be the same: precise, AI-assisted, and thoroughly reviewed. It has been a lot of fun.
There is documentation in the docs directory of the repo.