Office.js compatibility

The Office.js-compatible subset: which parts of the object model are implemented, how compatibility is verified in CI, and what it omits.

@docx-editor.dev/editor-api implements an Office.js-compatible object model. context.document.body.paragraphs, load() then sync(), search(text, options), getFirstOrNullObject() and isNullObject are the same members you already write. To port Word add-in code, change how the batch opens, then read the divergences below: a few of them cost your batch an extra sync().

Compatibility boundaries

  • The API runs outside the Office add-in host. It does not provide Office.onReady or Word.run. Open a batch with DocxEditor.createServer(bytes) or DocxEditor.createBrowser(editor). The callback uses the same object model.
  • This repository authors every public type. The package does not vendor, copy or generate code from Microsoft's declarations.
  • The API implements the subset listed below.

How compatibility is verified

Microsoft's published declarations provide reference-only conformance input. A maintainer script normalizes names and call shapes from a pinned @types/office-js release into a checked-in fixture. A checked-in manifest selects the symbols and members covered by compatibility checks.

CI verifies:

  • every allowed overload against the normalized fixture;
  • generated TypeScript assertions with the TypeScript compiler;
  • representative Word samples, changing only their namespace, against the authored declarations.

These checks read only repository files. Package installation, tests and builds do not fetch the reference declarations, and the published package contains no reference data.

What is implemented

The document, its body, paragraphs, ranges and the collections over them; search with matchCase / matchWholeWord; character formatting through font; paragraph formatting (style, alignment, indents, spacing); lists and list items; bookmarks; sections and page setup; footnotes and endnotes; comments with replies and a resolved flag; revisions with accept and reject, individually or all at once; and content controls, addressed by id, tag or title.

The lifecycle around them carries over unchanged: load() declares reads, sync() is the only round trip, and getFirstOrNullObject / getLastOrNullObject answer an object rather than throwing. A proxy survives every sync() inside the run that created it. To use it in a later run, add it to context.trackedObjects, return it, then adopt it with runtime.run(object, callback). A proxy that is not tracked is released when its run ends.

Omissions

The following APIs are absent from the public types, so code that uses them fails to compile:

OmittedCurrent behavior
Tables (Table, TableCollection, TableCell)The editor reads, lays out and paints tables, but this API cannot script them.
Images (InlinePicture, and picture list levels)This API does not expose image operations.
Floating shapes and canvases (Shape)The published subset does not expose shapes or canvases.
Repeating sections and picture content controlsOther content-control kinds are readable and writable.
Custom XML mapping (ContentControl.xmlMapping)A control bound to custom XML refuses writes because the API cannot maintain its binding.
Standalone hyperlinks (Hyperlink, HyperlinkCollection)Range.hyperlink reads and writes a range's hyperlink. The API does not expose standalone hyperlink objects.
getHtml, getOoxml, getText, existsThe API does not expose these methods or their ClientResult shapes. Body.text and Paragraph.text support loaded text reads.
Creating and deleting commentsThe API reads comments and supports replies and resolution. This release does not create or delete them.

Divergences

  • ContentControl.id is a string because DOCX files can repeat or omit the value.

  • ContentControl.subtype reports the file format's terms.

  • An item accessor answers an object that is not usable yet. getFirst(), getLast() and their OrNullObject forms need one await context.sync() before you can load or write through the object they return. Which object it is comes back from a read, and a batch has to name its targets before it is sent, so resolving it in place would mean several round trips per sync(). Using it too early throws InvalidObjectPath, and the message names the sync.

    const results = context.document.body.search('Total');
    await context.sync();
    
    const first = results.getFirst();
    await context.sync(); // the extra one
    
    first.insertText('TOTAL', 'Replace');
    await context.sync();
  • A collection loads its items, not their properties. paragraphs.load('text') and paragraphs.load('items/text') are refused with InvalidArgument. Load the collection, sync, then load what you want from the items, for the same reason: the items are not addressable until the first read has answered.

    const paragraphs = context.document.body.paragraphs;
    paragraphs.load();
    await context.sync();
    
    for (const paragraph of paragraphs.items) paragraph.load('text');
    await context.sync();

Migrating an add-in

  1. Replace Word.run(async (context) => { … }) with a runtime you construct: DocxEditor.createServer(bytes) on a server, DocxEditor.createBrowser(editor) in a page. The callback body is the part that stays the same.
  2. Expect the same load/sync discipline you already write. Reads still have to be declared, and a property you did not load still fails.
  3. Add the syncs the divergences above describe. The statements stay the same; a batch that reaches an item accessor or a collection's item properties needs one more round trip than it does in an add-in.
  4. Check the omissions above. If your add-in walks tables or inserts pictures, that work has no equivalent here yet.

Next steps

On this page