Skip to content

Commit 3817ba6

Browse files
authored
fix(super-editor): narrow exportDocx default return type for browser consumers (#2844)
The default exportDocx overload returned Promise<Blob | Buffer>, forcing browser consumers (e.g. Angular's fromPromise -> Observable<Blob>) to cast every call. The runtime value is decided by the editor's isHeadless flag at construction time, which the type system cannot see. - Default overload is now generic: exportDocx<T extends Blob | Buffer = Blob> - Browser consumers get Promise<Blob> automatically; Node headless consumers opt in with exportDocx<Buffer>() - Narrow overloads (exportXmlOnly, exportJsonOnly, getUpdatedDocs) and the runtime implementation are unchanged - Adds Editor.exportDocx.types.spec.ts covering all call shapes; uses .spec.ts so tsconfig.json type-checks it while tsconfig.build.json excludes it from the published dist
1 parent a22c0af commit 3817ba6

2 files changed

Lines changed: 80 additions & 2 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Type-level regression tests for Editor#exportDocx overload resolution.
3+
*
4+
* Runtime: vitest sees one trivial assertion and passes.
5+
* Compile-time: every annotated assignment inside `_typeOnlyAssertions` fails
6+
* `tsc --noEmit` if an overload ever stops resolving to the expected return
7+
* type. The function is never called — it exists solely to force the type
8+
* checker to evaluate the assignments.
9+
*
10+
* File uses `.spec.ts` (not `.test.ts`) so tsconfig.json still type-checks it
11+
* while tsconfig.build.json keeps it out of the published dist.
12+
*/
13+
14+
import { describe, it, expect } from 'vitest';
15+
import type { Editor } from './Editor.js';
16+
17+
// Never invoked — pure type-level assertions. Wrapped in a function so vitest
18+
// doesn't try to execute the assignments at module load time.
19+
20+
function _typeOnlyAssertions(editor: Editor): void {
21+
// Three narrow overloads.
22+
const _xmlOnly: Promise<string> = editor.exportDocx({ exportXmlOnly: true });
23+
const _jsonOnly: Promise<string> = editor.exportDocx({ exportJsonOnly: true });
24+
const _updatedDocs: Promise<Record<string, string | null>> = editor.exportDocx({ getUpdatedDocs: true });
25+
26+
// Default overload: T defaults to Blob, so browser consumers get
27+
// Promise<Blob> without casting.
28+
const _defaultNoArgs: Promise<Blob> = editor.exportDocx();
29+
const _defaultWithParams: Promise<Blob> = editor.exportDocx({ commentsType: 'external' });
30+
31+
// Bare call, no contextual type — the default `T = Blob` must fire here, or
32+
// consumers still get the `Blob | Buffer` union downstream.
33+
const _bareInferred = editor.exportDocx();
34+
const _proveBareIsBlob: Promise<Blob> = _bareInferred;
35+
36+
// Node-headless consumers opt in to Buffer.
37+
const _explicitBuffer: Promise<Buffer> = editor.exportDocx<Buffer>();
38+
const _explicitBlob: Promise<Blob> = editor.exportDocx<Blob>();
39+
40+
// Soundness guard: combining an explicit type argument with a narrow-flag
41+
// param must NOT compile. Without this guard, `<Buffer>({ getUpdatedDocs: true })`
42+
// typed as `Promise<Buffer>` while the runtime returned a file map.
43+
// @ts-expect-error getUpdatedDocs: true is incompatible with the default overload
44+
editor.exportDocx<Buffer>({ getUpdatedDocs: true });
45+
// @ts-expect-error exportXmlOnly: true is incompatible with the default overload
46+
editor.exportDocx<Buffer>({ exportXmlOnly: true });
47+
// @ts-expect-error exportJsonOnly: true is incompatible with the default overload
48+
editor.exportDocx<Buffer>({ exportJsonOnly: true });
49+
50+
// Customer scenario: Angular's `fromPromise` expects `Promise<T>` and yields
51+
// `Observable<T>`. Mirroring its shape — the default overload must flow into
52+
// `Promise<Blob>` without cast or type argument. (slack: p1776255665152579)
53+
const fromPromise = <T>(_p: Promise<T>): { value: T } => ({ value: undefined as unknown as T });
54+
const _angularBlob: { value: Blob } = fromPromise(editor.exportDocx({ commentsType: 'external' }));
55+
56+
// Silence unused-variable warnings.
57+
void _xmlOnly;
58+
void _jsonOnly;
59+
void _updatedDocs;
60+
void _defaultNoArgs;
61+
void _defaultWithParams;
62+
void _proveBareIsBlob;
63+
void _explicitBuffer;
64+
void _explicitBlob;
65+
void _angularBlob;
66+
}
67+
68+
describe('Editor#exportDocx overload resolution (type-only)', () => {
69+
it('passes when the file type-checks', () => {
70+
expect(true).toBe(true);
71+
});
72+
});

packages/super-editor/src/editors/v1/core/Editor.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3107,12 +3107,18 @@ export class Editor extends EventEmitter<EditorEventMap> {
31073107
* - `exportXmlOnly: true` → `string` (raw XML)
31083108
* - `exportJsonOnly: true` → `string` (JSON string)
31093109
* - `getUpdatedDocs: true` → `Record<string, string | null>` (file map)
3110-
* - Default → `Blob` (browser) or `Buffer` (Node.js headless)
3110+
* - Default → `Blob` (browser) or `Buffer` (Node.js headless). The runtime
3111+
* value is determined by the editor's `isHeadless` option at construction
3112+
* time, which the type system cannot see — so the default overload is
3113+
* generic with `Blob` as the default. Browser consumers get `Blob`
3114+
* automatically; Node headless consumers opt in with `exportDocx<Buffer>()`.
31113115
*/
31123116
async exportDocx(params: ExportDocxParams & { exportXmlOnly: true }): Promise<string>;
31133117
async exportDocx(params: ExportDocxParams & { exportJsonOnly: true }): Promise<string>;
31143118
async exportDocx(params: ExportDocxParams & { getUpdatedDocs: true }): Promise<Record<string, string | null>>;
3115-
async exportDocx(params?: ExportDocxParams): Promise<Blob | Buffer>;
3119+
async exportDocx<T extends Blob | Buffer = Blob>(
3120+
params?: ExportDocxParams & { exportXmlOnly?: false; exportJsonOnly?: false; getUpdatedDocs?: false },
3121+
): Promise<T>;
31163122
async exportDocx({
31173123
isFinalDoc = false,
31183124
commentsType = 'external',

0 commit comments

Comments
 (0)