strict mode: Enable TypeScript strict mode across the core-web workspace
@nicobytes is already working on this.
Since Aug 24, 2026.
- #36957 by @nicobytes — closed without merging
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
Enable TypeScript strict mode incrementally across the entire core-web Nx workspace, one project at a time, rather than a big-bang global strict: true.
Today core-web/tsconfig.base.json has "strict": false, and it stays that way — the rollout never flips it globally. Each project opts in via its own tsconfig.json, keeping every PR small and reviewable.
The rollout follows the project dependency graph bottom-up (leaf libraries first, applications last). When a project is activated, all of its internal dependencies are already strict, so the type errors surfaced are genuinely its own instead of leaked any from upstream.
Approach (updated)
The original plan was to use typescript-strict-plugin with an opt-in paths list. That approach was dropped. The bootstrap issue (#35933) was closed without the plugin ever landing — it is absent from package.json, pnpm-lock.yaml, and main — and the first merged rollout PR (#36879, dotcms-models) instead used per-project tsconfig.json flags. That is now the established pattern:
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
There is no tsc-strict script and no // @ts-strict-ignore escape hatch. Sub-issues still referencing either are stale — ignore those acceptance criteria.
What enforces strict: for Rollup libraries that emit declarations ("declaration": true), @rollup/plugin-typescript is in the build chain and reports type errors, so the existing build target is the gate — CI runs nx run-many -t build via the build-test execution in core-web/pom.xml. Angular libraries typecheck through ng-packagr. Vite-based projects are the exception: their builds use esbuild and skip type checking, which is why the Nx Vite plugin infers a separate typecheck target for them. Note that lint does not catch type errors — ESLint reports lint rules, not TS diagnostics.
See core-web/CLAUDE.md → "TypeScript Strict Mode" for the per-project procedure.
This Epic tracks one task per TS project (42 total), each its own small PR, executed in strict dependency order.
Excluded from scope: dotcms-scss (SCSS only), dotcms-ui-e2e (e2e), mcp-server, and the empty root core-web project.
Removed from the rollout — dead libraries
Two projects were dropped after investigation showed they are dead code that does not compile. Removal is tracked in #36950:
| Project | Issue | Why |
|---|---|---|
dotcms |
#35936 (closed) | Legacy initDotCMS SDK. 0 dependents, tagged skip:build/lint/test, broken dotcms-models import, last npm publish 2022-10-10. Superseded by @dotcms/client. |
dot-layout-grid |
#35937 (closed) | Legacy NgGrid grid. 0 dependents, no build target at all, imports ComponentFactoryResolver (removed from Angular), peer deps pinned to Angular 6/7. Last consumer deleted 2024-08-22. Superseded by GridStack in libs/template-builder. |
Process note: the sub-issues were generated from the Nx dependency graph without checking whether each project was still alive. An audit of all 44 confirmed the problem is bounded to these two — every other library in the rollout has real dependents — but future automated rollouts should verify liveness before generating issues.
Progress
| Issue | Project | Outcome |
|---|---|---|
| #35934 | dotcms-models |
Done — PR #36879 (established the per-project pattern) |
| #35935 | sdk-types |
Already compliant before the epic; PR #36957 shipped the docs |
| #35936 | dotcms |
Dead library — closed, removal tracked in #36950 |
| #35937 | dot-layout-grid |
Dead library — closed, removal tracked in #36950 |
| #35938 | sdk-create-app |
Done — PR #36957 (2 errors) |
| #35939 | dotcms-js |
Done — PR #36957 (38 errors) |
| #35940 | utils |
Done — PR #36957 (32 lib + 17 spec errors) |
| #35941 | sdk-uve |
Already compliant before the epic — closed, no diff |
| #35942 | sdk-client |
Already compliant before the epic — closed, no diff |
| #35943 | dotcms-webcomponents |
Groundwork only in PR #36957 — Stencil decorator members prepared; strict still off, ~250 errors across 38 files remain |
| #35944 | utils-testing |
Done — PR #36957 (flags were present but inert: a stale types: ["jasmine"] aborted all type checking) |
| #35945 | sdk-react |
Done — PR #36957 (14 TS4111 accesses; build verified as a real gate by negative test) |
| #35946 | sdk-analytics |
Done — PR #36957 (18 TS4111 in source + 29 in specs, 14 of them pre-existing drift; not enforced — negative test shows the Vite build ignores type errors) |
| #35947 | sdk-angular |
Already compliant — PR #36957 removed dead next/ tsconfig refs that made tsconfig.spec.json unverifiable (TS6053); build verified as a real gate by negative test |
| #35948 | data-access |
Done — PR #36957. Flags were present but inert (no build target, so nothing read its tsconfig): 36 lib + 47 spec errors fixed. Blast radius measured across all 7 strict dependents — zero new errors, 218 removed, three went 36 → 0. Not enforced |
| #35949 | sdk-experiments |
Done — flags only; enforcement proved by negative test (Rollup) |
| #35950 | portlets-dot-locales-data-access |
Already compliant — no diff; its 36 errors were data-access leaking |
| #35951 | global-store |
Done — one export type; the barrel's TS1205 was only visible from consumers |
| #35952 | portlets-dot-experiments-data-access |
Already compliant — no diff |
| #35953 | ui |
Partial — library program 0 (from 122), specs 90 (from 427). The rollout's bottleneck: ~109 errors leaked into each of its 26 dependents |
| #35954 | portlets-dot-analytics-data-access |
Compliant — its single error lived in global-store's barrel, closed by #35951 |
| #35960 | edit-content-bridge |
Done (2 errors). Enforced — has a build target |
| #35962 | portlets-dot-categories-portlet |
Done (31) — signal-mock pattern plus fixture drift in two directions |
| #35963 | portlets-dot-es-search-portlet |
Done (30) — all 28 spec errors were one assignment pattern |
| #35965 | portlets-dot-locales-portlet |
Done (27) — flags were present but unsatisfied; two wrong annotations |
| #35968 | portlets-dot-tags-portlet |
Done (22) — 18 were Signal↔jest.Mock casts |
| #35969 | portlets-dot-usage |
Done (1) — plus a global fix for the htmldiff-js type leak |
| #35970 | dotcdn |
Done (19), 8 from one missing switch default. Enforced. First app to go strict — surfaced template errors in libs/ui |
| #35953 | ui |
Done — 549 → 0. The rollout's bottleneck: ~109 errors leaked into each of its 26 dependents, so clearing it dropped seven projects from 219–275 to 0–59 |
| #35956 | new-block-editor |
Done (60) — 38 were TS4111 on TipTap attrs; EditorView sourced from @tiptap/pm/view; @types/turndown added |
| #35959 | content-drive-ui |
Done (59) — a drop with no active node emitted a payload its own type forbids |
| #35961 | portlets-dot-analytics |
Done (42) — Spectator's typed props vs Angular's input aliases; two type-only barrels; @types/d3-* added |
| #35966 | portlets-dot-plugins-portlet |
Done — reported 733, had 63. moduleResolution: node10 broke 256 imports. Also corrects libs/portlets/CLAUDE.md, which recommended that config |
| #35955 | block-editor |
Done (442, estimated 743) — four wrong declarations explained most of it; test suite was already red and stayed exactly as red (#37091) |
| #35973 | dotcms-block-editor |
Done — three tsconfigs had never type-checked anything (TS2688); unmasking them caught a template defect no tsc -p can see |
| #35971 | edit-ema-ui |
Done (119) — two stores declared their pre-render state non-nullable; a spec caught a wire-payload change |
| #35958 | template-builder |
Done (141) — plus 27 from a fixtures file being compiled as production code |
An Omit that erased a whole interface (#35955)
block-editor had DotMenuItem extends Omit<MenuItem, 'icon'>. PrimeNG's MenuItem carries a [key: string]: any index signature, so keyof MenuItem is string | number, and Exclude<string | number, 'icon'> removes nothing — Omit collapsed the type to its index signatures alone and discarded every declared member. id, label, command, disabled were all any, in a type exported to five dependents.
It is worth stating how this was established, because the obvious probe gives the wrong answer:
type Omitted = Omit<MenuItem, 'icon'>;
// Useless: true either way, because the index signature admits any string key.
type HasLabel = 'label' extends keyof Omitted ? 'PRESERVED' : 'WIPED';
// Decisive: `any` means only the index signature survived.
declare const viaOmit: Omitted['label']; // any → tsc reports nothing
declare const viaDirect: MenuItem['label']; // string | undefined → tsc reports it
Omit<T, K> is unsafe on any type with a string index signature. Worth grepping for elsewhere in the monorepo — it is a silent any factory, and nothing in the toolchain flags it.
Estimates were high because the graph was still dirty
block-editor was estimated at ~743 own errors and had 442. The difference is the libs/ui cascade: fixing that project removed ~300 errors from block-editor without anyone touching it. The same will be true of the remaining large projects, so their headline numbers should be re-measured, not planned against.
The cost of skipping the runtime check
block-editor:test is red on main — 16 suites, 37 tests, all Angular 22 migration debt (#37091). Strict mode landed anyway, with the suite verified unchanged by failing-test name rather than by count.
That is a real gap, not a formality. Earlier in this epic the compiler accepted a "completed" fixture that a test then rejected, because the omission the fixture made was the exact thing the test asserted on. On block-editor that check did not exist. Any project whose suite is red should have it repaired before, not after.
Two more ways to measure nothing (#35973, #35943)
Deprecated options abort too. TS5101 (baseUrl) and TS5107 (moduleResolution: node10) are config-level errors under TypeScript 6, so they stop the compiler before semantic checking exactly like TS2688 and TS6053. dotcms-webcomponents reports 2 errors without --ignoreDeprecations 6.0 and 279 with it. It cannot set the option in its tsconfig — Stencil bundles TypeScript 5.8.3, which only accepts "5.0", while the workspace runs 6.0.3, which requires "6.0".
The general rule is now in core-web/CLAUDE.md: any TS5xxx, TS6xxx or TS2688 error is a configuration error. tsc never reached the code, so whatever count follows is meaningless.
Check every tsconfig, not just spec. apps/dotcms-block-editor had "types": ["jasmine"] in three configs — spec, editor, and the tsconfig.json they share. All three had aborted on TS2688 since forever.
Templates are only gated where a build target exists
Closing dotcms-block-editor (#35973) turned out to matter far more than its zero own errors suggest. tsc -p was clean on all three configs, and then nx run dotcms-block-editor:build failed:
TS2339: Property 'contentlet' does not exist on type 'never'
libs/block-editor/.../suggestions-list-item.component.html
libs/block-editor has no build target, so its templates had never been type-checked by anything — tsc -p does not check them, and it was the only gate #35955 had. The small consumer app is what puts them under a real gate.
Worth carrying forward: for any project without a build target, "0 errors" means 0 errors in TypeScript files. Its templates are unverified until some consumer with a build compiles them.
A spec file that disabled type checking across three libraries (#35974)
libs/edit-content/.../dot-edit-content-field.component.spec.ts declared:
/* We need this declare to dont have import errors from CommandType of Tiptap */
declare module '@tiptap/core' {
interface Commands {
[key: string]: { [key: string]: (...args) => any };
}
}
Module augmentations are global to the TypeScript program, so this single spec gave TipTap's Commands a string index signature for every file compiled with it — which is why editor.chain().focus(), a real declared command, reported "Property 'focus' comes from an index signature".
edit-content's program pulls 249 files from block-editor and all of new-block-editor, so under strict flags it produced 256 errors in those two libraries (149 + 107). Both are clean under their own configs; nothing could see this until a consumer went strict.
Measured with and without it on the current non-strict config: 27 lib / 48 spec errors either way. The comment's premise was stale — it suppressed nothing and cost 256 unchecked sites. Removed.
A declare module in a spec file is a repo-wide change. Worth grepping for others.
Re-measured: the remaining estimates were wrong in both directions
Every project left in the rollout, measured by writing the flags into its own tsconfig.json (what the rollout actually does) rather than forcing them on the CLI — which matters, because CLI flags override a project's deliberate overrides:
| Issue | Project | Own errors | Epic estimate |
|---|---|---|---|
| #35976 | dotcms-binary-field-builder |
0 | — |
| #35967 | portlets-dot-query-tool-portlet |
3 | — |
| #35971 | edit-ema-ui |
119 | 185 |
| #35958 | template-builder |
141 | 220 |
| #35972 | portlets-content-drive |
269 | 316 |
| #35943 | dotcms-webcomponents |
279 | ~278 |
| #35964 | portlets-dot-experiments-portlet |
293 | 387 |
| #35957 | dot-rules |
381 | 657 |
| #35975 | portlets-edit-ema-portlet |
790 | 1112 |
| #35974 | edit-content |
943 | 1175 |
| #35977 | dotcms-ui |
2187 | ~1002 |
Roughly 5,400 rather than the ~6,000 implied, and dotcms-ui is the one that was under-estimated. dotcms-binary-field-builder and portlets-dot-query-tool-portlet are effectively free once edit-content lands — which makes edit-content the highest-leverage remaining project, not one of the two giants.
A fixtures file in the lib build (#35958)
template-builder reported 27 inherited errors, all Cannot find name 'jest' in libs/utils-testing. The cause was local: tsconfig.lib.json excludes *.spec.ts but not src/**/utils/mocks.ts, so a fixtures file importing @dotcms/utils-testing sat in the lib program with types: [].
edit-content reports the same 27 from the same cause, and it is worth grepping for others: a mocks.ts under src/ is production code unless the lib config says otherwise.
Zero values are not always the right default (#35971)
The bulk TS2564 policy — "string means '', boolean means false" — is wrong whenever the consumer's own default is not the zero value. @Input() showDiff: boolean became = false, but the dotDiff pipe defaults to true and DotContentCompareStore seeds showDiff: true. Diffing silently switched off and 4 specs failed.
Check what reads the field before picking its default.
Never let a type error change a payload (#35971)
publishContentletAndWaitForIndex takes { [key: string]: string | number }, and dot-favorite-page.store sends inode: formData.inode || null. The obvious fix is ?? '' — and it is wrong: the spec asserts inode: null because the endpoint distinguishes null from an empty string. The signature was the thing that needed widening.
This is the second time in this epic that the pull toward "make the error go away" would have altered behaviour. The first was a fixture that deliberately omitted a field its own test asserted on. Both were caught by running the tests, which is why a red suite (as in block-editor, #37091) is a real cost and not a formality.
On production non-null assertions
Bulk narrowing passes put 27 ! into edit-content's production files and 9 into template-builder's, each tripping @typescript-eslint/no-non-null-assertion — the rule that exists to discourage precisely that. Lint went from 11 warnings to 41, and from clean to 9.
They were reverted or converted to guards. Where a guard is not obvious, the error belongs on the remaining count rather than under an assertion: edit-content went back up because of this, deliberately. Strict mode is only worth the effort if the nullability is actually handled.
Enforcement is uneven and worth tracking. Only some of these projects have CI that actually verifies the flags.
- Enforced through their build:
sdk-types,sdk-create-app,sdk-uve,sdk-client,sdk-react,sdk-angular,sdk-experiments,edit-content-bridgeanddotcdn— the Nx rollup, esbuild, tsc and ng-packagr executors all type-check.sdk-angularwas confirmed by negative test (a deliberate type error failsnx run sdk-angular:buildwith TS2322). - Declared but unverified:
dotcms-js,utils,utils-testingand nowdata-accesshave nobuildtarget and are tag-excluded from lint and test, so their flags are documentation only — a regression there would not be caught.sdk-analyticslands in the same bucket for a different reason: it does build, but through Vite, and a negative test confirmed a deliberate type error does not failnx run sdk-analytics:build—vite-plugin-dtsemits declarations without failing on diagnostics. With five projects now in this state, this needs a single decision on whethernx affected -t typecheckgets wired intocore-web/pom.xml. - The opposite case:
dotcms-webcomponentshas noskip:build, so Stencil type-checks it on every PR. There the gate is real and strict is all-or-nothing — it cannot be switched on until the error count reaches zero.
A related trap found along the way: a tsconfig that names a types entry which is not installed makes tsc emit TS2688 and stop before semantic checking, so it reports one error no matter what the code does. That is what hid utils-testing's real errors, and it means a stable error count across a change proves nothing in that situation.
A second blind spot, found in #35946: the Nx-inferred typecheck target runs only tsconfig.lib.json, never tsconfig.spec.json. Combined with Vite projects transforming tests through babel-jest (types stripped, never checked), spec type errors are invisible to every gate — sdk-analytics had accumulated 14 of them unnoticed. Remaining projects should be measured against both configs, and should expect a similar hidden backlog.
A third finding, from #35947 — verify the compiler actually reached the code before trusting any count. Two of the fourteen projects triaged so far were silently unchecked: utils-testing via TS2688 (a types entry that is not installed) and sdk-angular via TS6053 (a files entry pointing at a deleted file). Both abort tsc before semantic checking, so it reports one error regardless of the code. sdk-angular's tsconfig.spec.json had never completed a single semantic pass. Note the asymmetry: a non-matching include glob is harmless, a missing files entry is fatal. The remaining ~30 projects' tsconfigs should be swept for both variants before their error counts are believed. core-web/CLAUDE.md now documents both.
A third, larger blind spot, found in #35948: nx run <project>:test does not type-check — anywhere in core-web. jest-preset-angular runs on ts-jest, and ts-jest copies TypeScript's isolatedModules into its own transpile-only switch (config-set.js:229), which stops it building the language-service host that diagnostics need (ts-compiler.js:74). This repo's Jest guidance requires isolatedModules: true in every tsconfig.spec.json, so the flag is set everywhere. data-access is the proof: 84 tsc errors alongside 754 passing tests. Consequences: (a) passing tests have never been evidence that specs type-check, so any prior per-project conclusion resting on that is void — the #35947 comment has been corrected on that basis; (b) every project in this rollout should be measured with tsc -p <projectRoot>/tsconfig.spec.json --noEmit, never with :test; (c) removing isolatedModules would switch checking on monorepo-wide, which is an epic-level call and was deliberately not made inside #35948. core-web/CLAUDE.md documents this alongside the two masking notes above.
A fourth measurement trap, from #35966: check moduleResolution before believing any count. dot-plugins reported 733 errors and had 63. Its tsconfig.spec.json used module: "commonjs" + moduleResolution: "node10", which cannot resolve the @dotcms/* subpath exports — so 256 imports failed and everything downstream collapsed to unknown (227 TS2571, 115 TS18046, 85 TS2339). Aligning it with dot-tags took it to 1. Every remaining project was swept and dot-plugins was the only one affected, so the large counts for block-editor, dot-rules, edit-content and edit-ema-portlet are real work rather than artefacts.
Root cause worth fixing at the source:
libs/portlets/CLAUDE.mdrecommended exactly this configuration. Its anti-patterns table said to omitstrict: true("causes issues with Angular compiler") and to use"module": "commonjs"intsconfig.spec.json, whiledot-tags— which the same guide calls the canonical reference — carries both and compiles clean. Corrected in batch three, so the next portlet does not inherit it.
The libs/ui cascade is the clearest evidence for working strictly bottom-up. It had none of the six flags and 549 own errors, ~109 of which leaked into each of its 26 dependents. Clearing it took portlets-dot-usage from 219 to 0 without touching that project, and portlets-dot-tags-portlet turned out to have 1 error of its own rather than 238. Re-measure after every upstream lands.
Dependencies added during the rollout. Four direct dependencies shipped no types, so their imports were implicitly any: d3-scale, d3-selection, d3-shape (#35961) and turndown (#35956). Their DefinitelyTyped packages are maintained, so they were installed rather than hand-declared. A fifth, htmldiff-js, has no DefinitelyTyped package and is declared in a root-level types/ folder registered through tsconfig.base.json — not next to its consumer, which is what made it leak one error into every consumer of libs/ui (#35969).
Two findings from batch two that change how the remaining projects should be measured.
1. tsc -p does not check templates. Every per-project count in this epic was taken with tsc, which misses template errors entirely — those are only verified when an app builds, and most projects here have no build target. Measured by probe across the three remaining apps: 2, 8 and 23 template errors against 350–2500 .ts errors. Real, but ~1% of the volume, so it does not change the plan. Found via #35970, the first app to go strict, whose build failed on libs/ui's templates rather than its own.
2. Upstream leakage inflates every count by roughly 60%. Measuring own-vs-inherited errors across the remaining 24 projects gave 8,916 own against 23,272 reported. The uniform ~109 seen in every portlet was libs/ui leaking through imports — portlets-dot-tags-portlet had 1 error of its own, not 238. Clearing libs/ui's library program dropped seven projects from 219–275 down to 0–59. The practical rule: fix strictly bottom-up and re-measure after each upstream lands, and never plan from a raw count. Corollary already visible — dotcms-block-editor, dotcms-binary-field-builder and portlets-dot-query-tool-portlet have almost no errors of their own but cannot reach 0 until block-editor, edit-content, new-block-editor and edit-ema are done.
Also worth noting: tsc -p on an app includes every file matching its include, while a build only compiles what is reachable from the entry point. For dotcms-ui that is 3,165 vs 1,002 — roughly two thousand errors sit in files that are never compiled or shipped.
Execution order (high level)
Layer 0 dotcms-models, sdk-types, ...
Layer 1 dotcms-js, utils, sdk-uve, ...
Layer 2 utils-testing, sdk-react, ...
Layer 3 data-access, ...
Layer 4 global-store
Layer 5 ui
Layer 6 block-editor + portlets-*
Layer 7 edit-ema-ui, portlets-content-drive
Layer 8 edit-content
Layer 9 portlets-edit-ema-portlet
Layer 10 dotcms-ui (app)
The full ordered list lives in the sub-issues, numbered [NN/44] (numbering kept as originally generated; two of those numbers are now closed as not applicable).
Desired Outcome
- All in-scope
core-webprojects compile clean understrictwith zero errors. - Every strict project is actually enforced by a CI target (build or
typecheck) — strict flags that nothing verifies are worthless. - No regressions:
nx affected -t build,lintstays green throughout. - New
anyusage trends toward zero.
Target Personas
- Developer teams
- Content teams
- DevOps teams
- System administrators (dotCMS)
Links
- Strategy: bottom-up topological order derived from
nx graph. - Established pattern: #36879 (
dotcms-models) — per-project tsconfig flags. - Dead-code removal: #36950
And a pattern worth stating up front for the remaining 29: flags present ≠ flags applied. data-access had all six flags plus Angular's strict template options for some time while carrying 36 errors, because a library with no build target never has its own tsconfig read by anything, and its dependents compile its sources under their own non-strict configs. Before assuming a project is done because its tsconfig looks right, check whether nx show project <p> lists a build target — and if not, run tsc -p yourself.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.