{"id":11451,"date":"2026-04-02T12:21:13","date_gmt":"2026-04-02T12:21:13","guid":{"rendered":"https:\/\/www.ericwhite.com\/blog\/?p=11451"},"modified":"2026-04-02T12:35:43","modified_gmt":"2026-04-02T12:35:43","slug":"announcing-openxmlsdkts-the-open-xml-sdk-for-typescript","status":"publish","type":"post","link":"https:\/\/www.ericwhite.com\/blog\/2026\/04\/02\/announcing-openxmlsdkts-the-open-xml-sdk-for-typescript\/","title":{"rendered":"Announcing OpenXmlSdkTs \u2014 The Open XML SDK for TypeScript"},"content":{"rendered":"<p>If you work with Office documents programmatically, you probably know the .NET <a href=\"https:\/\/github.com\/dotnet\/Open-Xml-Sdk\">Open-Xml-Sdk<\/a>. It&#8217;s been the gold standard for reading, writing, and manipulating .docx, .xlsx, and .pptx files at the XML level. But if you&#8217;re working in TypeScript or JavaScript \u2014 building Office add-ins, server-side document processing in Node.js, or browser-based document tools \u2014 you&#8217;ve been on your own.<\/p>\n<p><a href=\"https:\/\/github.com\/EricWhiteDev\/OpenXmlSdkTs\">OpenXmlSdkTs<\/a> brings the same programming model to TypeScript.<\/p>\n<h3>Why Build This?<\/h3>\n<p>For anyone building Word, Excel, or PowerPoint JavaScript\/TypeScript add-ins, direct Open XML manipulation is often the most reliable approach. But the JavaScript ecosystem has lacked a library that provides the kind of structured, typed access to Open XML packages that .NET developers are accustomed to.<\/p>\n<p>I built OpenXmlSdkTs to solve this problem. It provides the same class hierarchy, the same part navigation model, and the same LINQ to XML querying that makes the .NET SDK so productive \u2014 all in TypeScript, running in Node.js or the browser.<\/p>\n<p>Note that when programming with the Open-Xml-Sdk in dotnet, I invariably use LINQ to XML, not the various classes (Paragraph, Body, etc.) that represent the markup in the Open-Xml-Sdk. I don&#8217;t care for those classes, and instead prefer to use LINQ to XML for working with the markup. Maybe one day I will detail everything that I find wrong with those classes, but that is not the purpose of this post. In any case, this TypeScript library includes only the functionality for working with markup using LINQ to XML (LtXmlTs in this case).<\/p>\n<h3>What You Get<\/h3>\n<p><strong>Full document format support.<\/strong> Work with Word (.docx), Excel (.xlsx), and PowerPoint (.pptx) files.<\/p>\n<pre><code>OpenXmlPackage\n\u251c\u2500\u2500 WmlPackage        Word documents\n\u251c\u2500\u2500 SmlPackage        Excel spreadsheets\n\u2514\u2500\u2500 PmlPackage        PowerPoint presentations\n\nOpenXmlPart\n\u251c\u2500\u2500 WmlPart           Word parts (document, styles, headers, footers, etc.)\n\u251c\u2500\u2500 SmlPart           Excel parts (workbook, worksheets, charts, etc.)\n\u2514\u2500\u2500 PmlPart           PowerPoint parts (presentation, slides, masters, etc.)<\/code><\/pre>\n<p><strong>Three I\/O modes.<\/strong> Open and save documents as binary blobs (the standard ZIP-based format), Flat OPC XML strings (required for Office JavaScript\/TypeScript add-ins), or Base64 strings. The <code>open()<\/code> method auto-detects the format.<\/p>\n<p><strong>Pre-initialized namespace and element names.<\/strong> Static classes like <code>W<\/code>, <code>S<\/code>, <code>P<\/code>, and <code>A<\/code> provide pre-initialized <code>XName<\/code> and <code>XNamespace<\/code> objects for every element and attribute in the Open XML specification. Because these objects are <em>atomized<\/em> \u2014 two objects with the same namespace and local name are the same object \u2014 equality checks use identity comparison (<code>===<\/code>), giving excellent query performance. No more copy-pasting long namespace URIs.<\/p>\n<p><strong>Built on LINQ to XML for TypeScript.<\/strong> OpenXmlSdkTs is powered by <a href=\"https:\/\/www.npmjs.com\/package\/ltxmlts\" class=\"broken_link\">ltxmlts<\/a>, my TypeScript port of .NET&#8217;s LINQ to XML (which I wrote about in <a href=\"https:\/\/www.ericwhite.com\/blog\/2026\/03\/24\/linq-to-xml-for-typescript-why-i-built-it-and-how-claude-helped\/\">a previous post<\/a>). You get the full API \u2014 <code>elements()<\/code>, <code>descendants()<\/code>, <code>attributes()<\/code>, functional construction \u2014 all the things that make XML manipulation so much more pleasant than working with DOMDocument.<\/p>\n<h3>What the Code Looks Like<\/h3>\n<p>Here&#8217;s a concrete example. This loads a Word document, navigates to the comments part to read comment data, adds a new bold paragraph, and saves the result:<\/p>\n<pre><code>import { WmlPackage, W, XElement } from \"openxmlsdkts\";\nimport * as fs from \"fs\";\n\nconst buffer = fs.readFileSync(\"MyDocument.docx\");\nconst doc = await WmlPackage.open(new Blob([buffer]));\n\n\/\/ Navigate to the main document part and get its XML\nconst mainPart = await doc.mainDocumentPart();\nconst xDoc = await mainPart!.getXDocument();\n\n\/\/ Access comments using typed navigation\nconst commentsPart = await mainPart!.wordprocessingCommentsPart();\nif (commentsPart) {\n  const commentsXDoc = await commentsPart.getXDocument();\n  for (const comment of commentsXDoc.root!.elements(W.comment)) {\n    const author = comment.attribute(W.author)?.value ?? \"(unknown)\";\n    const text = Array.from(comment.descendants(W.t))\n      .map(t =&gt; t.value).join(\"\");\n    console.log(`Comment by ${author}: \"${text}\"`);\n  }\n}\n\n\/\/ Add a bold paragraph using functional construction\nconst body = xDoc.root!.element(W.body);\nconst sectPr = body!.element(W.sectPr);\nsectPr!.addBeforeSelf(\n  new XElement(W.p,\n    new XElement(W.r,\n      new XElement(W.rPr, new XElement(W.b)),\n      new XElement(W.t, \"Added by OpenXmlSdkTs.\")\n    )\n  )\n);\n\n\/\/ Save changes back\nmainPart!.putXDocument(xDoc);\nconst savedBlob = await doc.saveToBlobAsync();\nfs.writeFileSync(\"Modified.docx\", Buffer.from(await savedBlob.arrayBuffer()));<\/code><\/pre>\n<p>If you&#8217;ve used the .NET Open XML SDK, this should feel immediately familiar. The pre-atomized names (<code>W.p<\/code>, <code>W.r<\/code>, <code>W.t<\/code>, <code>W.b<\/code>) are the same element names you already know, and the nesting hierarchy in the functional construction mirrors the XML being produced.<\/p>\n<h3>Office Add-ins: First-Class Support<\/h3>\n<p>One of the key motivations for this library is building Office JavaScript\/TypeScript add-ins. For anyone targeting macOS or Office on the web, JavaScript add-ins are the only option. And if you need to manipulate the document at the Open XML level, you need to work with Flat OPC \u2014 a single-file XML representation of the entire package.<\/p>\n<h3>Lightweight and Focused<\/h3>\n<p>The library has three runtime dependencies: <a href=\"https:\/\/www.npmjs.com\/package\/jszip\" class=\"broken_link\">jszip<\/a> for ZIP compression and <a href=\"https:\/\/www.npmjs.com\/package\/ltxmlts\" class=\"broken_link\">ltxmlts<\/a> for LINQ to XML, and <a href=\"https:\/\/www.npmjs.com\/package\/sax\" class=\"broken_link\">sax<\/a>. It works in Node.js 18+ and modern browsers.<\/p>\n<h3>How Claude Helped<\/h3>\n<p>Like <code>ltxmlts<\/code>, I built OpenXmlSdkTs using Claude Code as my primary development tool. The approach was the same: targeted, methodical prompts \u2014 one class, one area of functionality at a time \u2014 with unit tests written and reviewed at each step. Claude handled the heavy lifting of translating patterns I knew from the .NET side into idiomatic TypeScript. But every design decision, every API surface choice, and every test case came from my experience with working with Open XML. This was intense, focused engineering using Claude as a powerful tool, not a magic wand.<\/p>\n<h3>MIT Licensed<\/h3>\n<p>OpenXmlSdkTs is released under the <strong>MIT License<\/strong> \u2014 the same license used by the C#\/.NET <a href=\"https:\/\/github.com\/dotnet\/Open-Xml-Sdk\">Open-Xml-Sdk<\/a>. Free for commercial and open-source use.<\/p>\n<h3>Get Started<\/h3>\n<p>Install from npm:<\/p>\n<pre><code>npm install openxmlsdkts<\/code><\/pre>\n<p>Both <a href=\"https:\/\/www.npmjs.com\/package\/openxmlsdkts\" class=\"broken_link\">openxmlsdkts<\/a> and its companion library <a href=\"https:\/\/www.npmjs.com\/package\/ltxmlts\" class=\"broken_link\">ltxmlts<\/a> are available on npmjs.<\/p>\n<p>Full documentation is available in the <a href=\"https:\/\/github.com\/EricWhiteDev\/OpenXmlSdkTs\">GitHub repository<\/a>, including an overview, per-class API reference docs, and runnable examples covering binary, Flat OPC, and Base64 round-tripping.<\/p>\n<p>If you&#8217;re building Office add-ins, processing documents on the server, or doing anything with Open XML in TypeScript \u2014 give it a try.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you work with Office documents programmatically, you probably know the .NET Open-Xml-Sdk. It&#8217;s been the gold standard for reading, writing, and manipulating .docx, .xlsx, and .pptx files at the XML level. But if you&#8217;re working in TypeScript or JavaScript \u2014 building Office add-ins, server-side document processing in Node.js, or browser-based document tools \u2014 you&#8217;ve [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"_s2mail":"yes","footnotes":""},"categories":[1],"tags":[],"class_list":["post-11451","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/posts\/11451","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/comments?post=11451"}],"version-history":[{"count":2,"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/posts\/11451\/revisions"}],"predecessor-version":[{"id":11453,"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/posts\/11451\/revisions\/11453"}],"wp:attachment":[{"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/media?parent=11451"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/categories?post=11451"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ericwhite.com\/blog\/wp-json\/wp\/v2\/tags?post=11451"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}