Skip to content

[RFC / Experiment] Enable strictNullChecks — full strict mode (stacked on #4004) - #4005

Open
MrRio wants to merge 3 commits into
typescript-internal-typesfrom
typescript-strict-null
Open

[RFC / Experiment] Enable strictNullChecks — full strict mode (stacked on #4004)#4005
MrRio wants to merge 3 commits into
typescript-internal-typesfrom
typescript-strict-null

Conversation

@MrRio

@MrRio MrRio commented Jul 16, 2026

Copy link
Copy Markdown
Member

Note

Stacked on #4004 (typescript-internal-types), which stacks on #4003. Review only the commits after the phase-2 head. This completes the strictness story: with this PR, jsPDF compiles under the full strict flag set.

What this does

Enables strictNullChecks — the one flag phase 2 deferred — and burns down the resulting 472 source errors and 72 spec errors to zero, with a hard rule enforced throughout: zero runtime behavior change. No new null guards, no default fallbacks, no early returns. The permitted tools were:

  • Honest nullable types where the value truly can be absent: loadFile() returns string | undefined, processJPEG() returns ImageProperties | null, AcroForm's Rect/Q fields are clearable, canvas width/height setters accept (and sanitize) anything, table()'s data param admits undefined because throwing "No data for PDF table." is its documented contract.
  • Control-flow-faithful narrowing that mirrors what the untyped code actually did (e.g. undefined > 0 === false becomes an explicit !== undefined && with identical truth table; isNaN(undefined) === true paths kept as ternaries).
  • Non-null assertions (!) where a real invariant guarantees presence — each non-obvious one carries a comment naming the invariant (event ordering, prereq chains, "first loop iteration assigns"). Deliberately-preserved legacy NaN-propagation arithmetic is asserted, not guarded.
  • Definite-assignment assertions for the used-before-assigned patterns.

Also per project policy on this stack: no new as unknown as casts anywhere in this diff (and two phase-2 casts became removable once the underlying types were honest, so they're gone).

Verification

Same gates as the stack, all green: npm run typecheck (src + test projects, now fully strict except nothing — every strict flag is on) at 0; lint (prettier + no-any/no-ts-ignore) clean; API parity identical to pre-port master; Node suite 467/0; browser suite 623 at the known environmental baseline; all five deployment suites at baseline; test-typings passes.

Notes for review

  • The riskiest edits are the honest-nullability widenings, since they touch declared contracts — each was verified against the implementation before widening (e.g. getPageInfo's validation genuinely coerces numeric strings, so its internal signature says so instead of hiding it behind a cast).
  • cell.ts/context2d.ts preserve some subtle legacy semantics on purpose: undefined/NaN propagation in cell arithmetic, and the mid-loop re-read of tableHeaderRow during recursive header printing.
  • With this merged, the natural end-state follow-up remains: generate the shipped types/index.d.ts from source and retire the hand-written file.

Addendum: the as unknown as purge

All 199 double-assertions inherited from phase 2 are now gone from src/ and the test suite, and a lint guard (lint-no-unsafe-cast, chained into npm run lint) fails the build if the pattern ever returns.

How they died, by category:

  • Reality declared instead of asserted: jsPDF's runtime static slots (__bidiEngine__, getPageSize) get ambient namespace-merged declarations; output/save/text/getCreationDate implementations are now properly-overloaded function declarations; jsPDFPrivate grew from an index signature to a fully-verified ~70-member interface (every member checked against the implementation); several signatures were honestly widened where the implementation genuinely accepts more (addImage's compression slot — which also surfaces the addSvgAsImage arg-shift latent bug in the type — newObjectDeferredBegin, kerning tables).
  • Negative tests use one sanctioned, documented primitive: invalidArg<T>(value: unknown): T — runtime identity, defined once in test/utils/compare.ts. Deliberately-invalid inputs are now greppable and self-describing (receiver-preserving at method call sites so this-binding survives — one purge regression was caught by the suite exactly this way and fixed).
  • Irreducible boundaries (the Blob/FileSaver polyfills that deliberately overwrite DOM globals with fakes; opaque vendored constructors): the chain is replaced by a named unknown-typed intermediate plus a single assertion. The laundering that remains is minimal, local, named, and commented — and the lint guard keeps it from ever silently spreading again.

Gate re-verified after the purge: both projects typecheck clean, lint (including the new guard) passes, API parity identical, Node 467/0, browser + all deployment suites at baseline.

MrRio added 3 commits July 16, 2026 05:48
…ange

- 472 src errors + 72 spec errors to zero under strictNullChecks
- techniques: honest nullable types (loadFile/processJPEG/getFont returns,
  acroform fields, canvas setters, table data param), control-flow-faithful
  narrowing, non-null assertions with invariant comments, and
  definite-assignment assertions; NO new runtime guards or defaults
- no new 'as unknown as' casts (user directive); two obsolete phase-2
  casts removed where honest types made them unnecessary
- getPageInfo internal signature honestly widened to number | string
  (its validation coerces; getPageInfoByObjId passes for-in keys)
- all 199 double-assertions removed from src and test
- src: reality declared instead of asserted — ambient namespace merging for
  jsPDF static slots (__bidiEngine__, getPageSize), overloaded function
  declarations for output/save/text/getCreationDate implementations,
  honest widenings (addImage compression slot, newObjectDeferredBegin,
  CharWidthsOptions.font), jsPDFPrivate is now a fully-declared ~70-member
  interface, GState carries its real reflective index signature
- irreducible boundaries (polyfills overwriting DOM types, vendored-opaque
  constructors) use named unknown-typed intermediates with single
  assertions — the chain pattern is gone everywhere
- tests: new sanctioned primitive invalidArg<T>() (runtime identity,
  declared in test/globals.d.ts, defined in test/utils/compare.ts) for
  deliberately-invalid negative-test inputs; receiver-preserving casts
  where methods are involved (this-binding survives)
- lint-no-unsafe-cast greps the tree and fails on any reintroduction,
  chained into npm run lint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant