any Cleanup Progress - details
packages/pdf-viewer
Session date: 2026-05-11
Files processed:
messageService.ts— 1 removed, 0 left. Replaceddata?: anywithdata?: Record<string, unknown>.Page.tsx— 1 removed, 0 left. Replacedlet style: anywithlet style: CSSProperties(imported fromreact).PdfDocument.ts— 3 removed, 0 left. ImportedPDFDocumentProxyandPDFPageProxyfrompdfjs-dist; typeddoc,pages(asRecord<number, PDFPageProxy>) and the localpdfDocument.
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/pdf-viewer/ clean.
packages/react-native-saf-x
Session date: 2026-05-11
Files processed:
src/index.ts— 1 removed, 0 left. Replaced{} as anywith{} as SafxInterface(the interface declared in the same file).
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/react-native-saf-x/ clean.
packages/default-plugins
Session date: 2026-05-11
Files processed:
build.ts— 4 removed, 0 left. ImportedArgvandArgumentsCamelCasefromyargs; typed builder callbacks as(yargs: Argv) => ...and handler args asArgumentsCamelCase<{ outputDir: string }>/ArgumentsCamelCase<{ plugin: string }>.
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/default-plugins/ clean.
packages/editor
Session date: 2026-05-11
Files processed:
CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts— 2 removed, 6 left.- Removed:
isPositiontype guard now usesPartial<DocumentPosition>instead ofany;removeOverlayoverlay param usesOverlayType<unknown>instead ofOverlayType<any>. - Left:
OptionUpdateCallbacknewVal/oldVal: any(the source isvalue: any, which is API-driven; narrowing in callbacks would be a logic change);addOverlayreturn type (theanystructurally deceives the base-class signatureSearchQuery | undefinedto allow returning{ clear: () => void }from the decorator branch — fixing it is a class-hierarchy refactor, not a typing tweak);commands as anycast (same kind of structural deception of the base class); and the 4 entries already tagged "CodeMirror 5 API requires any" / "Must match base class signature". Re-checked after the rule was clarified to allow small new type definitions — none of these are amenable to that.
- Removed:
CodeMirror/pluginApi/PluginLoader.ts— 4 removed, 1 left.- Removed: introduced
PluginLoaderWindowtype alias (Window & { __pluginLoaderScriptLoadCallbacks: Record<number, OnScriptLoadCallback>; __pluginLoaderRequireFunctions: Record<number, typeof codeMirrorRequire> }); replaced four(window as any).__pluginLoader…casts with(window as unknown as PluginLoaderWindow).…. - Left:
OnScriptLoadCallbackexports: any(already tagged "Plugin exports have dynamic structure").
- Removed: introduced
Files skipped entirely (only non-"Old code" tags inside):
types.ts—execCommand/varying argument types.CodeMirror/editorCommands/editorCommands.ts—EditorCommandFunctionvarying argument types.CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts— dynamic-extension test casts.
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/editor/ clean.
packages/utils
Session date: 2026-05-11
Files processed:
dom.ts— 1 removed, 0 left.findParentElementByClassNameparameter typedElement | null(broad enough to acceptEventTarget & Elementfrom callers in app-desktop).splitCommandString.ts— 1 removed, 0 left. Introduced localSplitCommandStringOptions { handleEscape?: boolean }interface.cli.ts— 0 removed, 1 left. TriedInterfacefromreadline/promises, but@types/nodein this repo does not declare thereadline/promisessubmodule. Updated reason on the disable comment to reflect that.execCommand.ts— 1 removed, 1 left.envtyped asRecord<string, string | undefined>. The other entry is already tagged with a Workaround reason (Expo/NodeJs.ProcessEnv conflict).net.ts— 1 removed, 0 left. Introduced localFetchWithRetryOptions extends RequestInitinterface withretry,callback,pausefields.object.ts— 0 removed, 2 left.objectValueFromPathdoes successive indexing (result = result[e]) which requiresany; tightened toRecord<string, unknown>failed because intermediate values areunknown.checkObjectHasPropertiesis called withNoteEntity/FolderEntity/ItemSlice(interfaces without index signatures) — tightening forces every caller to widen. Updated reasons to explain why.html.ts— 1 removed, 0 left.attributesHtml(attr: Record<string, string>)to match the local-only usage; renderer package has its ownattributesHtmlalready typed the same way.Logger.ts— 17 removed, 1 left.- Removed:
TargetOptions.consoletypedConsole;Logger.createwrapper argsunknown[];addTargetfield copy uses pairedRecord<string, unknown>casts;objectToStringouter paramunknown, inner Error branch uses a typed intersection cast;objectsToString,error/warn/info/debugrest argsunknown[];items: unknown[]; global-logger fallback typed as aLoggercast;consoleObj[fn]indexed throughRecord<string, (...args: unknown[]) => void>cast. - Left:
TargetOptions.database— tightening leaks throughlastEntries()to all downstream consumers (e.g.app-mobile/exportDebugReport.ts) that read.timestamp/.level/.messageviaany. Refactoring those is out of scope. Updated reason on the disable comment.
- Removed:
env.ts— 2 removed, 0 left.(error as Error).message = …;key_value = … as RegExpMatchArray(and the inner nested.match()typed the same way to preserve the original implicit non-null assumption).
Verification: yarn tsc --noEmit clean for the utils package; root yarn tsc --noEmit (all workspaces) clean — initial attempt broke 4 downstream files in app-desktop/app-mobile/server/tools, which forced reverts on checkObjectHasProperties and LoggerDatabase and a widening of findParentElementByClassName. yarn linter-ci packages/utils/ clean.
packages/renderer
Session date: 2026-05-11
Shared work: added a new RendererTheme interface in types.ts (a structural superset of the application theme that the renderer reads — cacheKey, appearance, colors, noteViewerFontSize, bodyPaddingTop/Bottom, buttonStyle, etc.). All fields are optional because callers pass either ThemeStyle from @joplin/lib, the renderer's defaultNoteStyle merge, or bare {} (tests). This made it possible to replace theme: any across the package without touching call sites in app-desktop, app-mobile, server, lib/commands/renderMarkup, etc. Also exported ResourceEntity from types.ts so Link.resource could be typed.
Files processed:
types.ts— 7 removed, 1 left.- Removed:
theme?,plugins?(typed asRecord<string, Record<string, unknown>>so it can be spread),MarkupRenderer.render,MarkupRenderer.allAssets,MarkupToHtmlConverter.render(also tightenedoptions: any→RenderOptions),MarkupToHtmlConverter.allAssets. - Left:
FsDriver.cacheCssToFile— return is used loosely as aRenderResultPluginAssetpush target while the actual returned object only has{ path, mime }; tightening forces logic changes. Updated reason on the comment.
- Removed:
HtmlToHtml.ts— 2 removed, 0 left. Boththeme: any→RendererTheme. Also madeOptions.ResourceModeloptional (constructor already handlesnull).MarkupToHtml.ts— 4 removed, 0 left.rawMarkdownIt_typed asMarkdownIt(added a type-onlyimport type * as MarkdownItType from 'markdown-it'). Theas anyon theRendererClassconstruction went away by branching to explicitnew MdToHtml(this.options_)/new HtmlToHtml(this.options_)calls.theme: anyonrenderandallAssets→RendererTheme.MdToHtml.ts— 19 removed, 4 left.- Removed:
RendererRule.install(typed viaPluginContext/RuleOptions/unknown),RendererRule.assets((theme: RendererTheme) => PluginAsset[]),RendererRule.plugin(MarkdownIt.PluginWithOptions);RendererPlugin.moduleandoptions(typed plugin +Record<string, unknown>);ExtraRendererRule.module(Omit<RendererRule, 'assetPath' | 'pluginId' | 'assetPathIsAbsolute'>);Options.pluginOptions(Record<string, { enabled?: boolean } & Record<string, unknown>>);Link.resource(ResourceEntity | null); the fourPluginContextanyfields (Record<string, string>for css,Record<string, PluginAsset[]>for pluginAssets,InMemoryCachefor cache,Record<string, Record<string, unknown>>for userData);RuleOptions.theme(RendererTheme);pluginOptions_(NonNullable<Options['pluginOptions']>);loadExtraRendererRule.module(matches newExtraRendererRule['module']);allProcessedAssets.theme,allProcessedAssets.assets(PluginAssets);allAssets.themeand innerassets(PluginAssets);render.theme(RendererTheme);_attrs: anyonhighlight()→string. - Left:
outputAssetsToExternalAssets_(output: any)— the function mutates withdelete output.cssStrings, whichRenderResultdoes not permit; updated reason on the comment.render'stheme: any = nullwas tightened totheme: RendererTheme = null(note: keepingnulldefault required RendererTheme to allownullvia callers passing it through, handled because all fields are optional).highlight()returnsanybecause the function returns either astringor{ wrapCode, html }depending on whetherrules.fenceis set — a non-standard markdown-it return.loadPlugin(plugin: any, options: any)— plugin may be a function or an ES-module wrapper with.default, and the function reassigns it before callingmarkdownIt.use(plugin, options); tightening forces logic restructuring. Updated reasons.
- Removed:
noteStyle.ts— 1 removed, 0 left.theme: any→RendererTheme(all fields optional, sotheme = theme ? theme : {}continues to type-check).headerAnchor.ts— 2 removed, 0 left. Added type-only imports forMarkdownItandmarkdown-it/lib/rules_core/state_core;markdownIt: any→MarkdownIt,state: any→StateCore.InMemoryCache.ts— 0 removed, 3 left. Generic cache: values are heterogeneous across calls (different keys store different shapes);unknownwould force narrowing changes at every caller. Updated reason on one comment that was tagged "Old code" to match the existing "Generic cache" reason.MdToHtml/setupLinkify.ts— 2 removed, 0 left. Added type-only imports forMarkdownItandlinkify-it;markdownIt: any→MarkdownIt,self: any→LinkifyIt.LinkifyIt.MdToHtml/linkReplacement.ts— 1 removed, 0 left.LinkReplacementResult.resource: any→ResourceEntity | null.MdToHtml/renderMedia.ts— 1 removed, 0 left.Options.theme: any→RendererTheme.
Verification at checkpoint (after top-level files, before processing MdToHtml/rules/*): package yarn tsc --noEmit clean; root yarn tsc --noEmit clean.
Shared work for the rules subdirectory: across most rules the markdownIt: any → MarkdownIt, state: any → StateCore / StateInline / StateBlock, tokens: any[] → Token[], Token: any → typeof import('markdown-it/lib/token'), and (tokens, idx, options, env, self) rule signatures pick up Renderer.RenderRule (or its component types). All these are type-only imports from markdown-it's @types package, so they have no runtime cost. Two RuleOptions extensions: globalSettings, settingValue (set per-rule by MdToHtml.render()), and mapsToLine (used only by source_map rule).
One unrelated typing fix: utils.getAttr(attrs: string[], ...) → (attrs: [string, string][], ...) — the function indexes attrs[i][0] / attrs[i][1], so the original string[] signature was wrong and prevented typing the tokens[idx].attrs calls in image.ts correctly.
Files processed:
MdToHtml/rules/abc.ts— 1 removed, 1 left.ruleOptions: any→RuleOptions(the existing comment said "we still don't have a type for ruleOptions"; we do now). Left:(self.renderToken as any)(tokens, idx, options, env, self)— extra args beyond the typed signature; reason already documented.MdToHtml/rules/code_inline.ts— 3 removed, 0 left. TypeddefaultRender: Renderer.RenderRule; outermarkdownIt: any→MarkdownIt; rule signature gets proper types.MdToHtml/rules/externalEmbed.ts— 0 removed, 2 left. Both(self.renderToken as any)(...)casts passenv, selfwhichrenderToken's typed signature does not declare. Updated reasons.MdToHtml/rules/checkbox.ts— 5 removed, 0 left.theme: any→RendererTheme;Token: any→typeof import('markdown-it/lib/token');sourceToken: any→Token;markdownIt: any→MarkdownIt;state: any→StateCore.MdToHtml/rules/fence.ts— 2 removed, 1 left. Typed the rule signature withToken[]/MarkdownIt.Options/Renderer. Left:options: anyon the fence renderer —options.highlighthere returns either a string or{ wrapCode, html }; the typed signature can't express that disjunction without restructuringMdToHtml.render.tmpToken as Tokencast added because the rule constructs a partialToken({ attrs }) just to callslf.renderAttrs, which only readsattrs.MdToHtml/rules/image.ts— 2 removed, 0 left.markdownIt: any→MarkdownIt; rule signature gets typed.MdToHtml/rules/fountain.ts— 4 removed, 0 left.theme: any→RendererTheme(the existing comment said "Theme is defined in @joplin/lib and we don't import it here" — but now we have a localRendererTheme);markdownIt: any→MarkdownIt; rule signatures typed.MdToHtml/rules/highlight_keywords.ts— 4 removed, 0 left.Token: any→typeof import('markdown-it/lib/token');markdownIt: any→MarkdownIt;state: any→StateCore.MdToHtml/rules/katex.ts— 9 removed, 0 left. IntroducedKatexMacroToken,KatexMacro,KatexOptionslocal interfaces (matching the macro shape documented in the file's own comments).stringifyKatexOptions,renderToStringWithCache, and the innertoSerializenow use these types.(t: any) => t.texttyped via theKatexMacroToken.textfield.state: any→StateInline/StateBlock;markdownIt: any→MarkdownIt; the two renderer functions typed viaToken[]. A singleas KatexOptions['macros']cast on the macros pulled fromoptions.context.userData.__katex.macros, sinceuserDatais typedRecord<string, Record<string, unknown>>and the renderer doesn't know the per-plugin shape there.MdToHtml/rules/html_image.ts— 6 removed, 0 left. Both default render fallbacks typed asRenderer.RenderRule;handleImageTagsnow returnsRenderer.RenderRule; outermarkdownIt: any→MarkdownIt; innercontent.replacecallback usesstringinstead ofany.MdToHtml/rules/link_close.ts— 3 removed, 0 left.defaultRender: Renderer.RenderRule;markdownIt: any→MarkdownIt; rule signature typed.MdToHtml/rules/mermaid.ts— 3 removed, 1 left.theme: any→RendererTheme;markdownIt: any→MarkdownIt; rule signature typed. Left: the(self.renderToken as any)(tokens, idx, options, env, self)cast in the fence-rule fallback — same pattern asabc.ts/externalEmbed.ts, passes extra args beyond the typedrenderTokensignature.MdToHtml/rules/sanitize_html.ts— 3 removed, 0 left.markdownIt: any→MarkdownIt;state: any→StateCore;tokens: any[]→Token[].MdToHtml/rules/link_open.ts— 2 removed, 0 left.markdownIt: any→MarkdownIt; rule signature typed.MdToHtml/rules/source_map.ts— 2 removed, 0 left.params: any→RuleOptions(addedmapsToLine?: booleantoRuleOptions); rule signature typed.
Verification: package yarn tsc --noEmit clean, yarn linter-ci packages/renderer/ clean, root yarn tsc --noEmit clean.
Summary: 99 → 12 disable comments. The 12 remaining are all either documented skip cases (cache values, as any to pass extra args to typed renderToken, output: any that mutates delete output.cssStrings, cacheCssToFile return used loosely, loadPlugin accepting both functions and { default } wrappers, the non-standard highlight() return shape, generic in-memory cache) or genuinely cannot be replaced without code-logic changes that are out of scope for this PR.
packages/tools
Session date: 2026-05-11
Note: the starting count of 49 included 3 disables inside packages/tools/node_modules/ (docusaurus and one in node_modules/@docusaurus/utils) which are vendored copies of third-party code and not in scope. In-scope was 46; after this session, 1 in-scope and 3 vendored remain (total 4).
Files processed:
postPreReleasesToForum.ts— 1 removed, 0 left.processedReleases: Record<string, any>→Record<string, boolean>(the stored value is alwaystrue).generate-images.ts— 1 removed, 0 left.output: any[]→(string | number | undefined)[](matches the heterogeneous values pushed fromOperation).generate-database-types.ts— 5 removed, 0 left. ImportedTableandColumnfrom@rmp135/sql-ts/dist/Typings;createRuntimeObject/generateListRenderDependencyType/the two.map()callbacks all typed via those. The column-push usesas Columnbecause adding the missingColumnDefinitionfields explicitly may changesql-ts.fromObject()output.update-readme-contributors.ts— 1 removed, 0 left. Therequestcallback typed(error: Error | null, response: { statusCode: number }, data: Contributor[]).release-clipper.ts— 1 removed, 0 left. Introduced localManifest = Record<string, unknown> & { background?, browser_specific_settings? };removeManifestKeys(manifest: Manifest): Manifest.update-readme-download.ts— 1 removed, 0 left.main(argv: any)→main(argv: string[])(called asmain(process.argv)).setupNewRelease.ts— 1 removed, 0 left. yargs.argvcast to{ _: string[]; updateVersion?: string; updateDependenciesVersion?: string }(rather thanyargs.Arguments<T>, becauseArguments._isArray<string | number>andparse-numbers: falseguarantees strings at runtime — using the library type would require a_[0] as stringcast at every call site).tool-utils.ts— 11 removed, 0 left.saveGitHubUsernameCache(cache: any)→Record<string, string>;execCommandoptions typed{ cwd?; env?; maxBuffer? }and its callback(error: (Error & {signal?: string}) | null, stdout: string, stderr: string);execCommandWithPipeserror: Error,code: number | null;setPackagePrivateField(value: any)→boolean;downloadFile'shttps.getcallbackresponse: import('http').IncomingMessage,error: Error;fileSha256streamdata: Bufferanderror: Error;fileExistsstat callbackerror: NodeJS.ErrnoException | null;gitHubLatestRelease/gitHubLatestRelease_KeepInCaseMicrosoftBreaksTheApiAgainresponse: anydeleted (node-fetch's inferredResponseworks fine);githubRelease(options: any)→{ isDraft?: boolean; isPreRelease?: boolean }.build-release-stats.ts— 0 removed, 1 left.createMarkdownTableis typed(headers, rows: MarkdownTableRow[])whereMarkdownTableRow = Record<string, string>.Releaserows contain numeric counts (windows_count, etc.), so the castrows as any[]masks a real type mismatch. Tightening would require wideningcreateMarkdownTablein@joplin/lib(cross-package change). Updated reason.licenses/licenseChecker.ts— 1 removed, 0 left.enforceString(line: any)→string | string[] | undefined | null(matches theArray.isArray(line) ? line.join(', ') : ...branches).website/updateNews.ts— 1 removed, 0 left. Introduced localRssFeedIteminterface (rssis untyped);feedItems: RssFeedItem[].website/processDocs.ts— 3 removed, 0 left.currentLinkAttrs?: any→[string, string][] | null; importedTokenfrommarkdown-it/lib/tokensoprocessToken(token: Token, …);onopentagattrs: Record<string, any>→Record<string, string>(matcheshtmlentities(attrs[n])usage and the localattributesHtmlsignature).website/build.ts— 1 removed, 0 left.scriptsToImport: any[]→{ id: string; sourcePath: string; md5: string; filename: string }[](the entries are all commented out, but the shape is implied by the loop that follows).website/utils/applyTranslations.ts— 1 removed, 0 left.onopentagattrs: any→Record<string, string>.website/utils/convertLinksToLocale.test.ts— 1 removed, 0 left.[string, any, string][]→[string, Partial<Locale>, string][]with alocale as Localecast at the call site (the test cases only supplypathPrefix, not the fullLocaleshape).website/utils/frontMatter.ts— 3 removed, 0 left.yaml.load(...)cast directly toFrontMatter;formatFrontMatterValue(value: any)→FrontMatter[keyof FrontMatter]; the(header as any)[key] = …assignment refactored to a separateRecord<string, string>output map.website/utils/frontMatter.test.ts— 1 removed, 0 left.testCases: any[][]→[string, FrontMatter, string, string][].website/utils/render.ts— 1 removed, 0 left.state: any→StateCore(imported frommarkdown-it/lib/rules_core/state_core).utils/translation.ts— 1 removed, 0 left. Introduced localGettextTranslationandGettextParsedinterfaces capturing only the fields read (gettext-parserisrequire-d and has no types).serializeTranslation's parameter inbuild-translation.tswas incorrectly typed asstring— corrected toParameters<typeof parseTranslations>[0].utils/discourse.ts— 5 removed, 0 left. IntroducedDiscourseApiError extends Error { apiObject; status };new Error(...) as DiscourseApiErrorthen settingerror.apiObject/error.statusdirectly.response.json() as anydeleted — node-fetch's inferred return works fine for the downstream.error.status === 404reads.createTopic/createPost/updatePostbody params →Record<string, string | number>(matchesexecApi).
Verification: package yarn tsc --noEmit clean, yarn linter-ci packages/tools/ clean, root yarn tsc --noEmit clean.
Summary: 49 → 4 disable comments; in-scope 46 → 1 (3 are inside node_modules/ vendored copies, not touched). The single in-scope remaining one is build-release-stats.ts, where fixing would require widening createMarkdownTable's signature in @joplin/lib.
packages/plugin-repo-cli
Session date: 2026-05-11
Shared work: imported PluginManifest from @joplin/lib/services/plugins/utils/types (plugin-repo-cli already depends on @joplin/lib). Most manifest: any and manifests: any parameters became PluginManifest and Record<string, PluginManifest>. The *.test.ts files in this package frequently use partial manifest fixtures, so a few as unknown as PluginManifest casts at call sites were needed to keep tests typing without bulking up every fixture with the full manifest_version/name/app_min_version set. Two helper functions were widened structurally (rather than via casts) where they only read a subset of fields: gitCompareUrl now takes Pick<PluginManifest, 'repository_url' | '_publish_commit'>, and getObsoleteManifests became a generic <T extends { _obsolete?: boolean }> so the existing-test fixtures still type-check.
Files processed:
index.ts— 13 removed, 0 left.extractPluginFilesFromPackagetypedexistingManifests: PluginManifests, returnPromise<PluginManifest>;files.find((f: any) => …)→(f: string)(sincefs.readdirreturnsstring[]);commitMessageparameters typed (manifest: PluginManifest | null,previousManifest: PluginManifest | null,error: Error | null);readManifests/writeManifestsreturn/takePluginManifests; the fourlet X: any = {}locals inprocessNpmPackagetyped; the yargs handlers refactored —setSelectedCommandand the three command handlers now share aCommandArgstype matching the{ pluginRepoDir, dryRun }shape bothcommandBuildandcommandUpdateReleaseexpect (the previousselectedCommandArgs = ''declaration also implicitly drifted from string → object);commands: Record<string, Function>replaced withRecord<string, (args: CommandArgs)=> Promise<void>>. The twoawait readJsonFile(...)callers in this file pass an explicit type parameter (readJsonFile<PluginManifest>(...),readJsonFile<{ version: string }>(...)) sincereadJsonFileis now generic.commands/updateRelease.ts— 3 removed, 0 left.(error: Error, assets: any)→assets: unknown(return is not consumed by callers);(resolve: Function, reject: Function)ban-types disable replaced with explicit(assets: unknown)=> void/(error: Error)=> void; introduced localPluginVersionStatsandPluginStatstypes and used them forcreateStats/saveStats.lib/checkIfPluginCanBeAdded.ts— 2 removed, 0 left.caseInsensitiveFindManifest/default-export both typed viaPluginManifestsandPluginManifest.lib/overrideUtils.ts— 3 removed, 0 left.applyManifestOverrides(manifests: PluginManifests, …); the innermanifest[propName] = propValuewrite usesas PluginManifest & Record<string, unknown>because we mutate via afor...ofoverObject.entries(override)keys.getObsoleteManifestsmade generic over<T extends { _obsolete?: boolean }>so the existing test (which passes manifest-shaped fixtures rather thanManifestOverride-shaped ones) types correctly without rewriting its data.lib/errorsHaveChanged.test.ts— 1 removed, 0 left.testCases: any[][]→[ImportErrors, ImportErrors, boolean][]; dropped theas anycast in the destructuring loop.lib/validateUntrustedManifest.ts— 1 removed, 0 left. Parameters typedmanifest: PluginManifest,existingManifests: Record<string, PluginManifest>.lib/updateReadme.test.ts— 1 removed, 0 left.manifests: any→Record<string, PluginManifest>.lib/updateReadme.ts— 2 removed, 0 left. Parametermanifests: Record<string, PluginManifest>; therows.push(manifests[pluginId])line uses anas unknown as MarkdownTableRowcast (the existing code stores PluginManifest objects inside aMarkdownTableRow[]andcreateMarkdownTableis typed forRecord<string, string>rows — the same impedance mismatch documented onbuild-release-stats.tslast session); therows.sort((a: any, b: any) => …)callback's args type-infer fromMarkdownTableRowdirectly.lib/utils.ts— 2 removed, 0 left.readJsonFilemade generic<T = unknown>(manifestPath, defaultValue: T = null): Promise<T>;isJoplinPluginPackage(pack: any)→{ keywords?: string[]; name: string }(matches the two fields it reads).lib/overrideUtils.test.ts— 2 removed, 0 left. First test'smanifestOverrides: any→Record<string, { _obsolete?: boolean; description?: string; [key: string]: unknown }>(matches the heterogeneous fixture). Second test'smanifests: any→ inferred from the literal.lib/gitCompareUrl.ts— 1 removed, 0 left. Widened toPick<PluginManifest, 'repository_url' | '_publish_commit'>(matches the fields the function actually reads, so the test fixtures don't need to spell out the wholePluginManifest).lib/checkIfPluginCanBeAdded.test.ts— needed touching even though it had no disable comments: the test passes partial fixtures, so call sites cast viaas unknown as Parameters<typeof checkIfPluginCanBeAdded>[0/1]. Same pattern inlib/validateUntrustedManifest.test.ts(as unknown as PluginManifest) andlib/gitCompareUrl.test.ts(where the tuple is typed[GitCompareManifest, GitCompareManifest | null, string | null]).
Verification: package yarn tsc --noEmit clean, yarn linter-ci packages/plugin-repo-cli/ clean, root yarn tsc --noEmit clean, yarn test (in plugin-repo-cli) clean — all 13 tests pass.
Summary: 33 → 0 disable comments. Every one was tagged Old code before rule was applied and could be replaced with PluginManifest, ImportErrors, structural picks, or generics.
packages/app-mobile
Session date: 2026-05-12
Note: starting baseline was 131 comments across 37 files. The grep also matched build artifacts under packages/app-mobile (compiled .js/.bundle.js.map) which are gitignored; the source counts (.ts/.tsx only) match the 131/37 baseline exactly.
Files processed (partial — checkpoint):
utils/debounce.tsx— 2 removed, 0 left. Replaced(...args: any[])with generic<Args extends unknown[]>(...args: Args)=> voidso callers likeNote.tsx's(event: EditorChangeEvent)=> voidstill type-check.utils/getVersionInfoText.ts— 1 removed, 0 left. Replaced(global as any).HermesInternalwith(global as { HermesInternal?: unknown }).HermesInternal.components/screens/ConfigScreen/types.ts— 1 removed, 0 left.UpdateSettingValueCallbackvalue paramany→unknown(callers don't read it as typed values).components/base-screen.ts— 1 removed, 0 left.Record<number, any>→Record<number, ReturnType<typeof StyleSheet.create>>.utils/database-driver-react-native.ts— 4 removed, 0 left.react-native-sqlite-storageships no.d.tsand no@types/...is installed, so introduced localSqliteDb/SqliteResultSetinterfaces covering the two methods used (executeSql, returning rows/insertId). The baseDatabaseDriverinterface usesSelectResult = any, so the narrower return type still satisfies it.utils/buildStartupTasks.ts— 1 removed, 0 left.resourceFetcher_downloadComplete(event: any)→event: { id: string; encrypted: boolean }(the shape emitted byResourceFetcher.eventEmitter_.emit('downloadComplete', ...)).index.web.ts— 1 removed, 0 left. Removed theRoot as anycast —Root extends React.ComponentandAppRegistry.registerComponent'sComponentProvideris() => React.ComponentType<any>, which() => Rootsatisfies.utils/fs-driver/fs-driver-rn.ts— 7 removed, 2 left (down from 9).- Removed:
appendFile/writeFilecontent paramany→string(base class usesstring);rnfsStatToStd_paramany→ localRnfsStatLikeunion ofStatResultT | ReadDirResItemT | DocumentFileDetailwithin-checks;readDirStatsoptionsany→ReadDirStatsOptions(withstats: RnfsStatLike[]);close(handle: any)→unknown(handle is ignored);readFileChunk(handle: any)→ typed inline{ path: string; offset: number; mode: string; stat: { size: number } | null };tarExtract/tarCreateoptionsany→Omit<Parameters<typeof tar*>[0], 'cwd'> & { cwd?: string }. - Left: the inner
output: any[]inreadDirStats— entries can be eitherDocumentFileDetail(SAF) or the normalizedStat-shaped object fromrnfsStatToStd_, the recursion helpers also accept this heterogeneous shape, so a single concrete type would force restructuring. Reason updated on the disable comment.
- Removed:
services/AlarmServiceDriver.android.ts— 2 removed, 0 left.@joplin/react-native-alarm-notificationships no types, so introduced localScheduledAlarm { id: string; data?: { joplinNotificationId?: number } }covering the two fields read.services/AlarmServiceDriver.ios.ts— 6 removed, 0 left. ImportedPushNotification,PushNotificationPermissions,ScheduleLocalNotificationDetailsfrom@react-native-community/push-notification-ios. Notes:requestPermissionsoptions{ alert: 1, badge: 1, sound: 1 }→ booleans (the typed interface declares them asboolean?; runtime accepts both, but the type is stricter).hasPermissionsreturn tightened toPromise<boolean>andperm.alert && perm.badge && perm.soundwrapped in!!()since the fields are optional booleans.scheduleNotification'siosNotificationkeepsid: stringand a finalas ScheduleLocalNotificationDetailscast because the library's typedScheduleLocalNotificationDetailsdeclares neitheridnoralertBody?— but the runtime requiresid(andalertBodymay be omitted), so the cast preserves existing behavior.root.tsx— 4 removed, 5 left.- Removed:
generalMiddlewareinnerscheduleRefreshFolders((action: any) => storeDispatch(action), ...)simplified toscheduleRefreshFolders(storeDispatch, ...);componentDidUpdate(prevProps: any)andUNSAFE_componentWillReceiveProps(newProps: any)typed asAppComponentProps(this also forced adding the missingsyncStarted: booleanfield thatmapStateToPropspopulates but the interface had not declared); the inner(action: any) => this.props.dispatch(action)simplified to(action) => .... - Left: the three top-of-file
anys on the middleware (storeDispatch: any,logReducerAction(action: any),generalMiddleware = (store: any) => (next: any) => async (action: any)) require typing redux actions across many call sites — significant refactoring;handleOpenURL_(event: any)—ShareExtension.shareURLhas a pre-existing typing inconsistency (declared()=> stringbut assigned both as a function and a string literal) so typing the event would expose that unrelated bug; oneawait reduxSharedMiddleware(store, next, action, storeDispatch as any)— kept asanycast since the upstream signature usesDispatchand storeDispatch is already typed loosely. Reasons updated on the disable comments.
- Removed:
In progress — packages with files still containing disable comments (see grep summary): components/ (ExtendedWebView, NoteEditor, NoteList, ScreenHeader, SelectDateTimeDialog, app-nav, plugins/backgroundPage, screens/{ConfigScreen, LogScreen, Note}, side-menu-content), contentScripts/, services/plugins/PlatformImplementation, utils/{appReducer, types}.
Second batch:
components/ExtendedWebView/types.ts— 1 removed, 0 left.OnMessageEvent.data: any→string(consumersJSON.parseit). Required casting thedatafield inindex.jest.tsx's test mock toas string(the mock passes pre-stringify values), and forced a typing tweak elsewhere.components/ExtendedWebView/index.tsx— 2 removed, 0 left.postMessage(message: any)→unknown(it's JSON-stringified).(props.style as any)cast removed by switching to array-stylestyle={[{...inline...}, props.style]}.components/ExtendedWebView/index.jest.tsx— 1 removed, 0 left.additionalProps: any→Record<string, unknown>; kept inline comment explaining the HACK.components/screens/ConfigScreen/SettingsToggle.tsx— 1 removed, 0 left.value: any→boolean.components/NoteList.tsx— 1 removed, 1 left.dispatch: (action: any)=> void→Dispatch(from redux). Left:styles_: Record<string, StyleSheet.NamedStyles<any>>—NamedStyles<T>requiresTto be the same record being passed; tightening tounknownbreaks property access. Reason updated.components/app-nav.tsx— 1 removed, 2 left.dispatchtypedDispatch;screenswidened toRecord<string, { screen: ComponentType<any> }>(typed wrapper, screens still have heterogeneous props). Left:route: any(NAV action with heterogeneous payload across NAV_GO/NAV_BACK/etc.) and the innerComponentType<any>for screens. Reasons updated.components/SelectDateTimeDialog.tsx— 3 removed, 0 left. ReplacedPureComponent<any, any>withPureComponent<SelectDateTimeDialogProps, SelectDateTimeDialogState>(local interfaces capturingthemeId,shown,date,onAccept,onRejectand state shape).components/ScreenHeader/index.tsx— 12 removed, 1 left. Added localtype ScreenHeaderStyles = ReturnType<typeof StyleSheet.create>and replaced every inner button factory'sstyles: anyparameter with it (12 occurrences). Left:styleObjectbuilder usesRecord<string, any>because it incrementally mixesViewStyle,TextStyle, andIconStyleentries spread from theme.icon — splitting into typed sub-objects would force restructuring. Reason updated.components/screens/ConfigScreen/SettingComponent.tsx— 3 removed, 0 left.value: any→unknown(callers pass arbitrary setting values; the inner branches narrow before forwarding).output: any = null→React.ReactElement | null = null.items as any(for the Dropdown options fromenumOptionsToValueLabels) →items as unknown as DropdownListItem[]becauseenumOptionsToValueLabelsreturns{ [computedKey]: string }[]where the runtime values happen to belabel/valuebut the type system can't tell. Forwarding toSettingsToggle/ValidatedIntegerInput/SettingTextInputadds explicit narrowing (!!props.value,props.value as number,props.value as string) per Setting type.components/screens/ConfigScreen/SectionSelector/index.tsx— 1 removed, 0 left.settings: Record<string, any>→Record<string, unknown>(the localSettingsMaptype isn't exported from@joplin/lib/components/shared/config/config-shared; the function accepts a wider record).components/screens/LogScreen.tsx— 2 removed, 1 left.navigation: any→{ state: { defaultFilter?: string } }(the only field accessed).navigationOptions(): any→: { header: null }. Left:styles: anybuilder — same heterogeneous-style-record pattern asScreenHeader.styleObject. Reason updated.components/screens/ConfigScreen/ConfigScreen.tsx— touched (not yet processed for disable count, butrenderToggle'svalue={value}→value={!!value}to satisfy the narrowedSettingsToggle.value: boolean).
Verification at checkpoint: package yarn tsc --noEmit clean; yarn linter-ci packages/app-mobile/ clean. Current state: 131 → 71 disable comments (60 removed).
Third batch (completing the package):
components/screens/ConfigScreen/ConfigScreen.tsx— 12 removed, 4 left.navigation: any→{ state?: { sectionName?: string } };navigationOptions(): any→: { header: null };onHeaderLayout/onSectionLayout/inlineonLayoutevents →LayoutChangeEvent(imported fromreact-native);renderButton/addSettingButtonoptions →{ description?: string; statusComp?: ReactElement; disabled?: boolean };handleSetting/settingToComponent/innerupdateSettingValuevalue: any→unknown;renderFeatureFlags'soutput: any[]→ReactElement[]. Left:settings: Record<string, any>on both state and props (and the parameters that take settings) — heterogeneous values (string/number/boolean/object) accessed by string key across many call sites;unknownforces casts at every read. Reason updated.components/screens/Note/Note.tsx— 13 removed, 4 left.emptyArray: any[]→never[];lastSavedNote: any→NoteEntity | null;styles_: any→Record<string, ReturnType<typeof StyleSheet.create>>;noteTagDialog_closeRequested: any→()=> void;refreshResource(resource: any)→ResourceEntity;focusUpdateIID_: any→ReturnType<typeof setTimeout> | null;folderPickerOptions_: any→FolderPickerOptions(imported from../../ScreenHeader);navigationOptions(): any→: { header: null };setState((state: any))→ inferred;onMarkForDownload(event: any)→{ resourceId: string };onPlainEditorSelectionChange(event: NativeSyntheticEvent<any>)→NativeSyntheticEvent<{ selection: SelectionRange }>;saveOneProperty(value: any)→unknown. Left:editorRef: any(union of Markdown/RichText/NoteBody viewers, each with different command surfaces);menuOptionsCache_: Record<string, any>(heterogeneous command/option entries);dialogbox: any(react-native-dialogbox ref, no library types);styles: anybuilder (heterogeneous view/text/icon style entries). Reasons updated.components/side-menu-content.tsx— 1 removed, 1 left.menuItems: any[]→PromptButtonSpec[](imported from./DialogManager/types). Left:syncReport: any— matchesstate.syncReport: anyin@joplin/lib/reducerandSynchronizer.reportToLines(report: any); tightening here would require updating the lib types first. Reason updated.components/NoteEditor/NoteEditor.tsx— 1 removed, 0 left.execCommand(command, ...args: any[])→unknown[].components/NoteEditor/hooks/useEditorCommandHandler.ts— 1 removed, 0 left.(...args: any[])→unknown[]; theargs[0]?.name/args[0]?.argsreads cast through{ name?: string; args?: unknown[] }.services/plugins/PlatformImplementation.ts— 2 removed, 1 left.Components.[key: string]: any→unknown;registerComponent(component: any)→unknown. Left:get nativeImage(): any— matchesBasePlatformImplementation.nativeImage: any. Reason updated.components/plugins/backgroundPage/initializePluginBackgroundIframe.ts— 1 removed, 0 left.(window as any).joplin = ...→(window as Window & { joplin?: unknown }).joplin = ....components/plugins/backgroundPage/utils/wrapConsoleLog.ts— 2 removed, 1 left.originalLog as any→as ((...args: unknown[])=> void) | undefined; outer wrapper signature(...args: any[])→unknown[]. Left: the} as any;cast at end ofwrapLogFunction— assigning a generic(...args: unknown[])=> voidto a specific console method requires going throughanybecause TypeScript treatsconsole[key]'s type as the intersection of all method signatures. Reason updated.components/NoteEditor/ImageEditor/ImageEditor.tsx— 1 removed, 0 left.onError(event: any)→WebViewErrorEvent(imported fromreact-native-webview/lib/WebViewTypes).contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.ts— 1 removed, 0 left.postMessageHandler(message: any): Promise<any>→unknown/Promise<unknown>(matchesContentScriptData.postMessageHandler: (message: unknown)=> unknownin@joplin/editor/types).contentScripts/rendererBundle/useWebViewSetup.ts— 1 removed, 0 left.pluginOptions: any→PluginOptions(imported from@joplin/renderer/MarkupToHtml); innerenabled: subValues[n]→enabled: !!subValues[n](PluginOptions expects boolean).contentScripts/rendererBundle/contentScript/Renderer.test.ts— 1 removed, 0 left.pluginSettings: Record<string, any>→Record<string, unknown>.contentScripts/imageEditorBundle/contentScript/index.test.ts— 1 removed, 0 left.window.ResizeObserver = class { ... } as any— added the missingunobserve()anddisconnect()methods so the class satisfies theResizeObserverinterface without a cast.root.tsx— 0 removed, 6 left (reasons updated).storeDispatch: any/logReducerAction(action: any)/generalMiddleware = (store: any) => (next: any) => async (action: any)/reduxSharedMiddleware(...storeDispatch as any)/handleOpenURL_: any— all kept asanybecause redux actions in this codebase don't have a single typed union; tightening would require a coordinated rewrite of every dispatched action across the mobile app. Reasons updated on every disable comment.utils/appReducer.ts— 0 removed, 8 left (reasons updated). The 3Old code before rule was appliedentries got their reason rewritten; the 5Assigning types... would be too big of a refactoringentries were already correctly tagged.utils/types.ts— 0 removed, 2 left (reasons updated).AppState.routeandAppState.noteSideMenuOptions— same redux NAV / per-screen heterogeneous-payload story as appReducer.utils/fs-driver/fs-driver-rn.ts— 0 removed, 1 left (reason already documented in the first batch).components/app-nav.tsx— 0 removed, 2 left (reasons documented in the second batch).components/NoteList.tsx— 0 removed, 1 left (StyleSheet.NamedStyles<any>— reason documented in the second batch).components/ScreenHeader/index.tsx— 0 removed, 1 left (heterogeneousstyleObjectbuilder — reason documented in the second batch).components/screens/LogScreen.tsx— 0 removed, 1 left (heterogeneousstylesbuilder — reason documented in the second batch).
Verification: package yarn tsc --noEmit clean; yarn linter-ci packages/app-mobile/ clean; root yarn tsc --noEmit (all workspaces) clean.
Summary: 131 → 33 disable comments (98 removed). The 33 remaining all fall into a small set of "structural" categories:
- Redux action/route shapes across
root.tsx(6),appReducer.ts(8),utils/types.ts(2),app-nav.tsx(2) — total 18. Tightening requires a discriminated action union across the mobile codebase. - Heterogeneous style-builder objects in
ScreenHeader/index.tsx(1),LogScreen.tsx(1),Note.tsx(1) — total 3. Each mixes ViewStyle/TextStyle/IconStyle entries built incrementally. - Heterogeneous redux state fields tied to lib's loose typing:
syncReport: anyinside-menu-content.tsx(1);nativeImage: anyinPlatformImplementation.ts(1) — total 2. Tightening requires updating the lib base types first. - Per-screen ConfigScreen settings:
settings: Record<string, any>inConfigScreen.tsx(4 occurrences: state, props,sectionToComponent,renderFeatureFlags) — total 4. Tightening tounknownforces casts at everysettings[key]read across the file. - Per-component fields that the library doesn't type:
editorRef/dialogbox/menuOptionsCache_/stylesbuilder inNote.tsx(4) — total 4. NamedStyles<any>inNoteList.tsx(1) — TypeScript pattern limitation.console[key] = ... as anyinwrapConsoleLog.ts(1) — TypeScript pattern limitation (intersection of console method signatures).output: any[]infs-driver-rn.ts(1) —readDirStatsoutput mixes SAFDocumentFileDetailand normalizedStat-shaped entries.
packages/server
Session date: 2026-05-12
Starting baseline 2026-05-12 matches the progress doc: 227 disable comments across 67 files, all tagged Old code before rule was applied.
General observation (different from prior packages): many of the server's anys sit at junction points (Koa context, Knex query callbacks, view contents, error payloads) where tightening propagates through many call sites. So this package will have a much lower remove-rate than the front-end packages.
First batch:
utils/strings.ts— 1 removed, 0 left.yesOrNo(value: any)→unknown.utils/array.ts— 1 removed, 1 left.removeElementtyped generically as<T>(array: T[], element: T). Left:unique(array: any[])— tried generic<T>, butBaseModel.loadByIdscalls it withstring[] | number[]and TS can't unifyTacross the union, forcing a cast at the call site (a wider blast radius). Reason updated.utils/cache.ts— 4 removed, 0 left.CacheEntry.object: any→string(always JSON-stringified);setAny/setObjecto: any→unknown;getAnyreturnPromise<any>→Promise<unknown>(existingas objectcast at the one public consumer remains valid).utils/errors.ts— 3 removed, 0 left.ErrorOptions.details?: any→unknown;ApiError.details: any→unknown;errorToPlainObject(error: any)→unknownwith'httpCode' in errornarrowing followed by(error as { httpCode?: number }).httpCodecasts on each field read (TS'sinoperator doesn't narrowunknownto a typed shape, only toobject).services/MustacheService.ts— 1 removed, 1 left. The locallayoutView: anyinrenderView→Record<string, unknown>. Left:View.content?: any— tried tightening toRecord<string, unknown>, butrouteHandler.ts:61readsview.content.error.httpCodeand other dynamic paths; views contribute heterogeneous content shapes per route. Reason updated.
Files attempted but reverted (still any):
commands/BaseCommand.ts—run(argv: any). Triedyargs.Arguments, but subclasses (e.g.CompressOldChangesCommand,StorageCommand) narrowargvto a per-commandArgvinterface, and TS function-parameter contravariance forbids that without making the whole class generic — which would propagate through everyBaseCommand[]consumer. Reason updated.utils/urlUtils.ts—setQueryParameters(query: any). Callers pass KoaParsedUrlQuery(Record<string, string | string[]>), pagination shapes with numbers, and plain string records. Tightening forces fixes at every call site. Reason updated.config.ts—initConfig(overrides: any). TriedPartial<Config>, butConfig.resourceDir: stringis required and only set by some test overrides; the existing spread relies onanyto bypass the missing-field issue. Reason updated.
Verification at checkpoint: package yarn tsc --noEmit clean. 227 → 217 disable comments (10 removed).
Second batch:
models/KeyValueModel.ts— 3 removed, 0 left. The twoas anycasts invalue<T>use the existing type parameter (as T). The localvalue: anyinreadThenWritebecomesawait this.value<Value>(key)— the explicit type param uses the publicValue = number | stringalready defined in the file.models/BackupItemModel.ts— 1 removed, 0 left.add(content: any)→string | Buffer(the only runtime caller passes a JSON string; the storage type isBuffer). Inner assignment usescontent as Buffer.models/UserItemModel.ts— 1 removed, 0 left. Droppedas anyonloadByIds(options.byUserItemIds as any)—byUserItemIdsis already typednumber[]andloadByIdsacceptsstring[] | number[].models/UserDeletionModel.ts— 0 removed, 1 left.end(error: any): triedError, but tests pass plain strings. TriedError | string, buterrorToStringrequiresError; wrapping strings innew Error()changes runtime output (adds astackfield to the serialized payload). Reason updated.models/utils/pagination.ts— 4 removed, 0 left.requestPaginationOrder(query: any)→ParsedUrlQuery | PaginationQueryParamswithas string/as PaginationOrderDirnarrowing on the read fields;requestPagination(query: any)→(Pagination & PaginationQueryParams) | null;filterPaginationQueryParams(query: any)→PaginationQueryParams | null;paginateDbQuerymade generic over<T = unknown>forPaginatedResults<T>and the localorderSql: any[]inferred from.map.utils/views/table.ts— 2 removed, 0 left.Table.requestQuery?: any→PaginationQueryParams;makeTablePagination(query: any)→ParsedUrlQuery(imported fromquerystring).utils/views/select.ts— 0 removed, 2 left. TriedRecord<string, unknown>, but callers pass concrete entity types likeUser(no index signature). Reason updated.models/ChangeModel/ChangeModel.ts— 1 removed, 0 left.requestDeltaPagination(query: any)→ChangePagination | null.models/ShareModel.ts— 2 removed, 0 left.shareUrl(query: any)→Record<string, string | number>;itemCountByShareIdPerUser'sgroupBy('user_id') as any→as unknown as { item_count: number; user_id: Uuid }[](Knex's typed builder doesn't carry the aggregate column shape throughdb.raw).utils/testing/koa/FakeRequest.ts— 2 removed, 0 left. Introduced localFakeNodeRequest { method?: string }(the only field used).utils/testing/koa/FakeResponse.ts— 4 removed, 0 left.body: any→unknown;headers_: any→Record<string, string>;set/getparams/return →string.utils/testing/fileApiUtils.ts— 2 removed, 0 left.getDeltareturn type and inner castPaginatedResults<any>→PaginatedResults<unknown>.models/items/storage/testUtils.ts— 1 removed, 0 left.let error: any = null→Error & { code?: CustomErrorCode }.
Verification at checkpoint: package yarn tsc --noEmit clean. 227 → 193 disable comments (34 removed).
Third batch:
utils/prettycron.ts— 17 removed, 0 left. Allnumbers: any[]→number[];numberToDateName(value: any, type: any)→(value: number | string, type: 'dow' | 'mon')(the function doesvalue - 1, so wrapping inNumber());dateList(numbers: any[], type: any)→(numbers: number[], type: 'dow' | 'mon'); introduced localtype LaterSchedule = Record<string, number[]>and used it forremoveFromSchedule,scheduleToSentence;removeFromSchedule(schedule, member: any, length: any)→(LaterSchedule, string, number); the fourcronspec: any/numDates: anyhandlers typed asstring/number; the final(window as any).prettyCroncast tightened to(window as Window & { prettyCron?: Record<string, unknown> }).prettyCron.utils/routeUtils.test.ts— 4 removed, 0 left. ThreetestCases: any[]typed as tuple arrays ([string, string, string, ItemAddressingType][],[string, {...}][],[string, string[]][]).routes: Record<string, any>typedRecord<string, number>with threeas unknown as Parameters<typeof findMatchingRoute>[1]casts at call sites (the test injects numbers in place ofRouterinstances).routes/api/sessions.test.ts— 8 removed, 0 left. Eight(context.response.body as any).idcasts →as { id: string }.routes/api/items.test.ts— 4 removed, 0 left.tree: any→Record<string, Record<string, null>>;PaginatedResults<any>(×2) →<unknown>;result.items as any→as unknown as SaveFromRawContentResult.routes/api/shares.test.ts— 3 removed, 0 left.tree: any→Record<string, Record<string, null>>; bothPaginatedResults<any>→<Share>and<{ user: { email: string }; status: ShareUserStatus }>(the test only reads those fields).routes/index/users.test.ts— 3 removed, 0 left.postUser(props: any)→Partial<User>;patchUser(user: any)→Partial<User> & Record<string, unknown>;as any).valueon aquerySelector→querySelector<HTMLInputElement>('input[name=email]').value.routes/admin/users.test.ts— 2 removed, 0 left.postUser(props: any)/patchUser(user: any)typedRecord<string, unknown>(tests intentionally passmax_item_size: ''whichPartial<User>would reject).routes/index/stripe.test.ts— 2 removed, 0 left.WebhookOptions.stripe?: any→ReturnType<typeof mockStripe>;simulateWebhook(object: any)→Record<string, unknown>.routes/index/shares.link.test.ts— 2 removed, 0 left.getShareContent(query: any)→Record<string, string>; the inneras anyreturn cast →as string | Buffer.routes/api/share_users.ts— 2 removed, 0 left.bodyFields<any>→bodyFields<{ status?: number }>;items: any[]→Record<string, unknown>[].routes/api/share_users.test.ts— 1 removed, 0 left.PaginatedResults<any>→<{ share: { id: string } }>.
Verification at checkpoint: package yarn tsc --noEmit clean; spellcheck clean. 227 → 145 disable comments (82 removed).
Fourth batch:
routes/api/batch.ts— 3 removed, 0 left.SubRequest.body: any/SubRequestResponse.body: any→unknown;SubRequestResponse.header: Record<string, any>→Record<string, unknown>.routes/api/batch_items.ts— 2 removed, 0 left.PaginatedResults<any>→<unknown>; the inneras anycast →as unknown as unknown[].models/UserModel.test.ts— 3 removed, 0 left. The threesyncInfo*: anytest fixtures share a single inline object-shape type with optionalppk(the third variant deletes it).routes/index/login.ts— 1 removed, 0 left.makeView(error: any)→Error | null.routes/index/home.test.ts— 1 removed, 0 left.context.response.body as any→as string.routes/index/items.test.ts— 1 removed, 0 left.items: any→Record<string, Record<string, never>>.models/items/storage/StorageDriverS3.ts— 2 removed, 0 left. Introduced localReadableLikeinterface (only the 3 listener overloads used) sostream2bufferis typed; the S3 SDK return is an opaque union, so cast at the call site:stream2buffer(response.Body as ReadableLike).models/items/storage/StorageDriverS3.test.ts— 1 removed, 0 left.parse: any→StorageDriverConfig & { enabled?: boolean }.routes/api/users.ts— 1 removed, 0 left.bodyFields<any>→bodyFields<Partial<User>>(fromApiInputaccepts a partial user).routes/api/users.test.ts— 1 removed, 0 left.results: any→getApi<{ items: User[] }>.routes/api/ping.test.ts— 1 removed, 0 left.body as any→as { status: string; message: string }.models/LockModel.test.ts— 2 removed, 0 left.'wrongtype' as any→as unknown as LockType(and the same forLockClientType).db.migrations.test.ts— 2 removed, 0 left.dbSchemaSnapshotreturn →Awaited<ReturnType<typeof sqlts.toTypeScript>>; thedb as anycast →as unknown as Parameters<typeof sqlts.toTypeScript>[1].utils/testing/testUtils.ts— 4 removed, 3 left.createItemTree(tree: any)→Record<string, unknown>;createItemTree3(tree: any[])→ localItemTree3Nodeinterface;checkContextError'sbody: any→ cast throughas { code?: ErrorCode };setupAppContext({} as any, ...)→as unknown as AppContext. Left:AppContextTestOptions.request: any(httpMocks.RequestOptionsis too narrow; callers passfiles: { file: { path: string } }and free-form bodies);appContext: anyinsidekoaAppContext(intentionally mocks only a subset ofAppContext, cast at return); thecreateBaseAppContextone was removed. Reasons updated on the two remaining ones.utils/requestUtils.ts— 2 removed, 7 left. Two safe removals: the outerIncomingMessagecast informParseusesas unknown as FormParseRequest; thebodyFields/bodyFilesreq: anytypedIncomingMessage. The other entries (BodyFields,FormParseResult.files,FormParseRequest.body,convertFieldsToKeyValuereturn) were attempted but reverted —Record<string, unknown>breaksFields/Filescompatibility (formidable'sFile | File[]union surfaces.filepathaccess errors), andBodyFieldswidening propagates to every route handler that readsbody.fields.emailetc. without narrowing. Reasons updated on the remaining ones.utils/routeUtils.ts— 5 removed, 1 left.Response.response: any/constructor(response: any)→unknown;internalRedirect(...args: any[])→unknown[];ExecRequestResult.response: any→unknown;respondWithItemContent(koaResponse: any)→ localKoaResponseLikeinterface with justbodyandset(). Left:RouteHandler's...args: any[], Promise<any>— concrete handlers (login, mfa, users) narrowargsto per-route field types; tightening propagates through every route. Reason updated. (Plus a downstream castresponseObject.response as typeof ctx.responseinrouteHandler.ts:56for the now-unknownresponse.)models/ChangeModel/ChangeModel.test.ts— 1 removed, 0 left.itemsToCreate: any[]→{ id: string; children: never[] }[].models/ChangeModel/ChangeModel.old.ts— 1 removed, 0 left.Knex.Raw<any>→Knex.Raw<unknown>.routes/api/items.ts— 1 removed, 0 left.bodyFields.items.map((item: any))→(item: { name: string; body?: string }).tools/generateTypes.ts— 1 removed, 0 left.'pascal' as any→as Config['tableNameCasing'].models/utils/pagination.test.ts— 2 removed, 0 left.testCases: any→[Record<string, unknown> | null, Pagination][]; the inlineinput: anyremoved by inferring from the tuple. Inner literaldir: 'asc'switched toPaginationOrderDir.ASC.
Verification at checkpoint: package yarn tsc --noEmit clean; spellcheck clean. 227 → 102 disable comments (125 removed).
Fifth batch:
utils/joplinUtils.ts— 11 removed, 1 left. TightenedFileViewerResponse.body/ResourceInfo/LinkedItemInfoto concrete shapes (Buffer | string,NoteEntityetc.);unserializeJoplinItem/serializeJoplinItemtyped againstNoteEntity;getResourceInfosoutput uses the namedResourceInfosalias;jopItemanditemToRenderuseNoteEntity & { ...optional fields };FileToRender.content→Buffer | null(thenull as anycast is no longer needed). Left: therenderOptions: anyformarkupToHtml.render—@joplin/renderer'sRenderOptionsis loosely typed in that package; tightening would require updating renderer first.db.ts— 14 removed, 0 left.ConnectionCheckResult.error/latestMigration: any→Error | nulland{ name: string } | null; the slow-query handlerconnection: any, bindings: any[]→DbConnectionandunknown[]; the innerqueryInfos: Record<any, QueryInfo>andtimeoutId: any→Record<string, QueryInfo>andReturnType<typeof setTimeout>;filterBindings(bindings: any[]): Record<string, any>→unknown[]/Record<string, unknown>;KnexQueryErrorData.bindings: any[]→unknown[];migrateList'smigrations: anytyped as the actual[string | { file } | { name | file }][]tuple via aMigrationInfoalias;isNoSuchTableError/isUniqueConstraintErrorerror: any→{ code?: string; message?: string } | null | undefined. ThepgsetTypeParsercallback'sval: any→string.utils/testing/apiUtils.ts— 11 removed, 0 left. All thebody: Record<string, any>parameters (×9 functions) →body: object(entity types likeUser,FormUserdon't have an index signature, soRecord<string, unknown>rejects them —objectaccepts both concrete entities and plain records). Thequery: Record<string, any>→Record<string, unknown>(callers always pass plain objects).
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean. 227 → 67 disable comments (160 removed).
Sixth batch:
models/ItemModel.ts— 4 removed, 3 left.SaveFromRawContentResultItem.error→ union ofError & { httpCode?: number; code?: string }/PlainObjectError/null(callers read.httpCodeon it;errorToPlainObjectis also assigned to it).objectToApiOutputinneras any[k]casts →(output as Record<string, unknown>)[k].allForDebugtyped(Omit<Item, 'content'> & { content?: string | Buffer })[]and now spreads instead of mutating. Left:itemToJoplinItem(): any(heterogeneous Note/Folder/Resource/Tag return); the matchingjoplinItem?: anyfield; the innerjoplinItem: anylocal. Reasons updated.models/BaseModel.ts— 7 removed, 0 left.SaveOptions.validationRules/previousItem,DeleteOptions.validationRules,ValidateOptions.rules→Record<string, unknown>.all()rows: any[]cast →as T[]only.fromApiInputlocaloutput: any→Record<string, unknown>withas Record<string, unknown>on the input spread andas Ton the return.isNew's'id' in (object as any)→typeof object === 'object' && object && 'id' in objectnarrowing.app.ts— 7 removed, 0 left.defaultEnvVariables: Record<Env, any>→Record<Env, Partial<EnvVariables>>.markPasswords(o: Record<string, any>)→objectparameter (concrete entity types likeDatabaseConfiglack index signatures) with an internal cast toRecord<string, unknown>for iteration.getEnvFilePath(argv: any)→{ envFile?: string }.argv: Argv = yargsArgv as any→as unknown as Argv. ThecommandArgv._cast →as Argv & { _: string[] }.main().catch((error: any))→(error: Error).utils/testing/testRouters.ts— 3 removed, 2 left.execcallbackerror/stdout/stderr: any→(Error & { signal?: string }) | null/string/string.serverProcess: any→ReturnType<typeof spawn>.checkAndPrintResult(result: any)→unknownwithinnarrowing. Left:curlreturn type (it parses heterogeneous JSON responses that callers read without narrowing) and theresponse: anyinmain()(same reason). Reasons updated.utils/testing/shareApiUtils.ts— 5 removed, 0 left. Introduced localLegacyTreeNodeandShareTreeNodeinterfaces (with[key: string]: unknownonShareTreeNodeso test fixtures can include arbitrary fields liketitle).convertTree(tree: any)/createItemTree3(tree: any[])/shareFolderWithUser(itemTree: any)use those types.routes/admin/users.ts— 5 removed, 0 left.boolOrDefaultToValue/intOrDefaultToValue/makeUserfields: any→Record<string, unknown>. The field reads inmakeUsercast viaas string/as numberper User shape.error: anyonadmin/users/:idroute handler →Error | null.accountTypeOptions().map((o: any))→(o: { value: number; selected?: boolean }). TheformParse().fieldsis castas unknown as Record<string, unknown> & { id?: Uuid }at the makeUser call.
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean. 227 → 36 disable comments (191 removed).
Final batch:
routes/index/stripe.ts— 4 removed, 0 left.stripeEvent(req: any)→IncomingMessage;StripeRouteHandlerreturn →Promise<unknown>; the two(postHandlers as any)[path.id]casts collapsed into a single typed lookup.env.ts— 4 removed, 0 left.parseEnv(defaultOverrides: any)→Partial<EnvVariables>. The three(output as any)[key]writes go through a singleoutputAsRecord = output as unknown as Record<string, unknown>alias.routes/index/users.ts— 3 removed, 0 left.makeUser(fields: any)→Record<string, unknown>withas stringnarrowing on field reads;error: anyonusers/:idGET →Error | null;accountTypeOptions().map((o: any))→(o: { value: number; selected?: boolean }).models/UserModel.ts— 2 removed, 1 left.(resource as any)[key]/(previousResource as any)[key]→as Record<string, unknown>.syncInfo(): Promise<any>→Promise<{ ppk?: { value: PublicPrivateKeyPair } }>. Left:checkMaxItemSizeLimit(joplinItem: any)— same heterogeneous itemToJoplinItem return as ItemModel.middleware/routeHandler.ts— 1 removed, 0 left.const r: any = { error }→ typed{ error: string; stack?: string; code?: string }.
Verification: package yarn tsc --noEmit clean; yarn linter-ci packages/server/src/ clean; root yarn tsc --noEmit (all workspaces) clean.
Summary: 227 → 22 disable comments (205 removed). Zero remaining Old code before rule was applied disables — all 22 left have descriptive reasons explaining why they can't be tightened. They fall into these categories:
- Heterogeneous redux/Koa shapes (5):
BaseCommand.run(argv: any)(per-command Argv narrowing forbids the base type from being typed without making the class generic);routeUtils.RouteHandler(per-route argument types);joplinUtils.renderOptions(renderer package's RenderOptions is loose);MustacheService.View.content(each view contributes different fields);testUtils.appContext(Koa mock only provides a subset). - Heterogeneous Joplin item types (3):
ItemModel.itemToJoplinItemplus the twojoplinItem: anylocals it feeds;UserModel.checkMaxItemSizeLimit.joplinItem. - Heterogeneous error shapes (1):
UserDeletionModel.end(error: any)— tests pass strings while runtime callers pass Errors. - Concrete entity types without index signatures (3):
urlUtils.setQueryParameters(query: any);select.tsyesNoOptions/yesNoDefaultOptions;app.tsconfig initConfig overrides. - Loose lib-typed defaults (1):
config.tsinitConfig(overrides: any)—Partial<Config>requiresresourceDir. - Heterogeneous test fixtures / API call results (3):
testRouters.curlandresponseinmain(parsed JSON responses);array.tsunique(TS can't unifyTacrossstring[] | number[]). - TypeScript pattern limitations (6):
testUtils.AppContextTestOptions.request; the 4 inrequestUtils.ts(BodyFields,FormParseResult.files,FormParseRequest.body,convertFieldsToKeyValue— all centered on formidable'sFields | Filesunion not allowing narrowing).
packages/app-desktop
Session date: 2026-05-13Branch: any_refactor_6
Files processed:
commands/exportFolders.ts— 1 removed, 0 left._context: any→CommandContext.commands/exportNotes.ts— 1 removed, 0 left._context: any→CommandContext.commands/focusElement.ts— 1 removed, 0 left._context: any→CommandContext.commands/toggleExternalEditing.ts— 1 removed, 0 left.mapStateToTitle(state: any)→AppState.checkForUpdates.ts— 0 removed, 1 left.parentWindow: anyis passed tobridge().showMessageBox(parentWindow, ...)but the bridge signature isshowMessageBox(message: string, ...)— tightening would expose a pre-existing call-site mismatch (logic change). Reason updated.gui/ConfigScreen/ButtonBar.tsx— 1 removed, 0 left.type StyleProps = any→ local interface with theme fields used in template literals.gui/ConfigScreen/controls/plugins/PluginBox.tsx— 1 removed, 0 left.styled.div<{ mb: any }>→string | number.gui/ConfigScreen/controls/plugins/SearchPlugins.tsx— 1 removed, 0 left.onPluginSettingsChange(event: any)→OnPluginSettingChangeEvent(already used by callers).gui/DialogButtonRow.tsx— 1 removed, 0 left.okButtonRef?: any→React.Ref<HTMLButtonElement>.gui/DialogButtonRow/useKeyboardHandler.ts— 1 removed, 0 left.isInSubModal(targetElement: any)→EventTarget | null.gui/Dropdown/Dropdown.tsx— 1 removed, 0 left.onChange(event: any)→React.ChangeEvent<HTMLSelectElement>.gui/EditFolderDialog/Dialog.tsx— 1 removed, 0 left.onFolderTitleChange(event: any)→React.ChangeEvent<HTMLInputElement>.gui/EditFolderDialog/IconSelector.tsx— 0 removed, 1 left.(window as any).EmojiButton— emoji-button library is dynamically loaded onto window with no published types. Reason updated.gui/ErrorBoundary.tsx— 1 removed, 0 left.componentDidCatch(error: any)→Error | string(matches thetypeof === 'string'narrowing already in the body); needsas Errorafter the narrowing branch.gui/NoteEditor/NoteBody/CodeMirror/utils/index.ts— 1 removed, 0 left.cursorPos: any→{ line: number; ch: number }(CodeMirror 5 position shape used inside the function).gui/NoteEditor/NoteBody/CodeMirror/utils/types.ts— 1 removed, 0 left.pluginAssets: any[]→RenderResultPluginAsset[](from@joplin/renderer/types).gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts— 0 removed, 1 left.(editorRef.current as any).alignSelection— CodeMirror 5 runtime method not in the type. Reason updated.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useExternalPlugins.ts— 0 removed, 1 left.CodeMirror: any— receives dynamically-loaded CM5 namespace used for plugin registration; @types/codemirror's signature is too narrow. Reason updated.gui/NoteEditor/commands/focusElementNoteBody.ts— 1 removed, 0 left.comp: any→WindowCommandDependencies.gui/NoteEditor/commands/pasteAsMarkdown.ts— 1 removed, 0 left. Same.gui/NoteEditor/commands/pasteAsText.ts— 1 removed, 0 left. Same.gui/NoteEditor/commands/showLocalSearch.ts— 1 removed, 0 left. Same.gui/NoteEditor/commands/showRevisions.ts— 1 removed, 0 left. Introduced localShowRevisionsDependencies(NoteEditor passes a different shape withsetShowRevisions/isInFocusedDocumentfor this command only — not registered viauseWindowCommandHandler).gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.ts— 0 removed, 1 left.CodeMirror: any— same reason asuseExternalPlugins.ts. Reason updated.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useKeymap.ts— 0 removed, 1 left. Same.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils.ts— 0 removed, 1 left. Same.gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useContentScriptRegistration.ts— 1 removed, 0 left.postMessageHandler(message: any)→unknown(consumed byemitContentScriptMessagewhich already acceptsunknown).gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.tsx— 1 removed, 0 left.onChange(event: any)→React.ChangeEvent<HTMLTextAreaElement>.gui/NoteEditor/NoteBody/TinyMCE/utils/joplinCommandToTinyMceCommands.ts— 1 removed, 0 left.value?: any→string(every literal in the file uses string values).gui/NoteEditor/NoteBody/TinyMCE/utils/useEditDialog.ts— 0 removed, 1 left.onSubmit(dialogApi: any)— TinyMCE dialog API not in published types. Reason updated.gui/NoteEditor/editorCommandDeclarations.test.ts— 1 removed, 0 left.Record<string, any>→Record<string, boolean>(every literal in the file is a boolean).gui/NoteEditor/utils/contextMenu.ts— 1 removed, 0 left.saveFileData(data: any)→string | NodeJS.ArrayBufferView(matchesfs.writeFile's accepted data types).gui/WindowCommandsAndDialogs/commands/addProfile.ts— 1 removed, 0 left.comp: any→WindowControl;onClose(answer: string)→unknownwithas string(matchesPromptOptions.onCloseshape).gui/WindowCommandsAndDialogs/commands/moveToFolder.ts— 1 removed, 0 left. Same —WindowControl.gui/WindowCommandsAndDialogs/commands/renameFolder.ts— 1 removed, 0 left. Same; onClose answer narrowed via cast.gui/WindowCommandsAndDialogs/commands/renameTag.ts— 1 removed, 0 left. Same.gui/OneDriveLoginScreen.tsx— 0 removed, 1 left.React.Component<any, any>— old class component without state/props refactor. Reason updated.gui/ResizableLayout/ResizableLayout.tsx— 1 removed, 0 left.newSize: any→{ width?: number; height?: number }.gui/ResizableLayout/utils/layoutItemProp.ts— 1 removed, 0 left.(item as any)[propName]→propName: keyof LayoutItem;item[propName]is typed; caller (MainScreen.tsx) needsas stringfor previously-looseitem.context.pluginId.gui/ResizableLayout/utils/persist.test.ts— 1 removed, 0 left.layout: any→LayoutItem(the type was already imported).gui/ResizableLayout/utils/style.ts— 0 removed, 1 left. styled-components.attrstyping collides with the dynamic style attrs (props.size). Reason updated;(props: any)callback now typed inline.gui/ResizableLayout/utils/types.ts— 1 removed, 0 left.context?: any→Record<string, unknown>.gui/ResizableLayout/utils/useWindowResizeEvent.ts— 1 removed, 0 left.eventEmitter: any→{ current: { emit: (name: string) => void } }.gui/Root.tsx— 0 removed, 1 left.React.Component<Props, any>— implicit state shape; tightening requires structural refactor. Reason updated.gui/Sidebar/styles/index.ts— 1 removed, 0 left.type StyleProps = any→ local interface with theme + item discriminators.gui/StatusScreen/StatusScreen.tsx— 0 removed, 1 left.style: anyis spread intotheme.containerStyle(loosely typed). Reason updated.gui/SyncWizard/Dialog.tsx— 1 removed, 0 left.boxes: any[]→React.ReactNode[].gui/NoteEditor/utils/useDropHandler.ts— 1 removed, 0 left.editorRef: any→RefObject<NoteBodyEditorRef>. Tightening exposed twoexecCommand()floating-Promise calls; fixed withvoid(samevoidpattern as showLocalSearch).gui/NoteEditor/utils/useMessageHandler.ts— 1 removed, 0 left.editorRef: any→RefObject<NoteBodyEditorRef>.gui/NoteEditor/utils/useNoteSearchBar.ts— 1 removed, 0 left.noteSearchBarRef: MutableRefObject<any>→MutableRefObject<HTMLInputElement | null>.gui/NoteEditor/utils/usePluginServiceRegistration.ts— 1 removed, 0 left.ref: any→Ref<unknown>(must useRef, notRefObject, to acceptForwardedRefcallers).gui/NoteEditor/utils/useSearchMarkers.ts— 1 removed, 0 left.keywords: any[]→{ value: string; type?: string; accuracy?: string }[](matches bothKeyword-consuming code and the literal fromuseNoteSearchBar).gui/NoteList/utils/useOnNoteClick.ts— 1 removed, 0 left.(event.target as any)→HTMLElement.gui/NoteList/utils/useScroll.ts— 1 removed, 0 left.event: any→React.UIEvent<HTMLDivElement>; switchedevent.targettoevent.currentTarget(target isEventTargetwithout scrollTop).gui/NoteListControls/commands/focusSearch.ts— 1 removed, 0 left.searchBarRef: any→{ current: { select: ()=> void } | null }.gui/NoteListHeader/useDragAndDrop.test.ts— 1 removed, 0 left.as any→as InsertAt(now exported fromuseDragAndDrop).gui/NoteListHeader/utils/validateColumns.test.ts— 1 removed, 0 left.(props: any)→Partial<NoteListColumn>[].gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.ts— 1 removed, 0 left.comp: any→WindowControl.
Checkpoint 5 (2026-05-13):
gui/WindowCommandsAndDialogs/commands/showNoteProperties.ts— 1 removed, 0 left.comp: any→WindowControl.gui/WindowCommandsAndDialogs/commands/showShareFolderDialog.ts— 1 removed, 0 left. Same.gui/WindowCommandsAndDialogs/commands/showShareNoteDialog.ts— 1 removed, 0 left. Same.gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.ts— 0 removed, 1 left.Menu.buildFromTemplate(menuItems as any)—spellCheckerConfigMenuItemsreturns a heterogeneous menu shape that doesn't satisfy Electron'sMenuItemConstructorOptionsstructurally. Reason updated.gui/WindowCommandsAndDialogs/commands/toggleNotesSortOrderField.ts— 1 removed, 0 left.field?: string | any[]→string | [string, boolean].gui/WindowCommandsAndDialogs/utils/useWindowControl.ts— 1 removed, 0 left.onClose(answer: any)→unknownwithas PromptSuggestion<T>cast inside.gui/dialogs.ts— 1 removed, 0 left.options: any = null→Record<string, unknown>.gui/hooks/usePrevious.ts— 1 removed, 0 left. Dangling disable comment (noanyin the body) — removed.gui/style/StyledInput.tsx— 1 removed, 0 left.type StyleProps = any→ local interface with theme fields.gui/utils/convertToScreenCoordinates.ts— 1 removed, 0 left.o: any→unknownwith internalRecord<string, unknown>narrowing afterJSON.parse(JSON.stringify(o)).integration-tests/util/setMessageBoxResponse.ts— 0 removed, 1 left. Mock return cast prevents breakage when Electron'sMessageBoxReturnValueshape evolves. Reason updated; redundant block comment removed.tools/notarizeMacApp.ts— 0 removed, 1 left.appBundleIdis no longer in@electron/notarize'sNotaryToolStartOptionsbut still required at runtime. Reason updated.utils/checkForUpdatesUtilsTestData.ts— 3 removed, 0 left.releases1/2/3: any→unknown as GitHubRelease[]cast at the end of each array literal (fixtures include extra GitHub API fields not inGitHubRelease).utils/checkForUpdatesUtils.test.ts— 1 removed, 0 left.testCases: [any, …][]→[GitHubRelease[], …][].gui/Navigator.tsx— 0 removed, 1 left.ScreenProps = any: heterogeneous per-screen prop shapes; the navigator just spreads them. Reason updated.gui/NewWindowOrIFrame.tsx— 1 removed, 0 left.createPortal(...) as anycast wasn't needed;ReactPortalalready assignable toReactNode.gui/DropboxLoginScreen.tsx— 0 removed, 2 left. Reasons updated (old class component + JS module without exported type).gui/Sidebar/Sidebar.tsx— 0 removed, 2 left.syncReport: anymatches the lib reducer shape; the innersyncCompletedWithoutErroralready had a descriptive reason. Both reasons now explain why they can't be tightened locally.gui/Button/Button.tsx— 2 removed, 0 left.type StyleProps = any→ local theme interface;ref: any→React.Ref<HTMLButtonElement>.gui/ExtensionBadge.tsx— 2 removed, 0 left.style?: any→React.CSSProperties;themeSelector(_state: any, props: any)→ typed byProps.gui/ImportScreen.tsx— 2 removed, 0 left. LocalProgressStateinterface;onError(error: any)→Error.gui/MultiNoteActions.tsx— 2 removed, 0 left.notes: any[]→NoteEntity[];multiNotesButton_click(item: any)→MenuItem(electron).gui/NoteRevisionViewer.tsx— 2 removed, 0 left. Dangling disable onrevisionList_onChangeremoved;webview_ipcMessage(event: any)→{ channel?: string; args?: unknown[] }.gui/ResizableLayout/utils/setLayoutItemProps.ts— 2 removed, 0 left.props: any→Partial<LayoutItem>;(item as any)[n]→(item as unknown as Record<string, unknown>)[n].gui/ResizableLayout/utils/useLayoutItemSizes.ts— 2 removed, 0 left.noWidth/HeightChildren: any[]→{ item: LayoutItem; parent: LayoutItem }[].gui/ToggleEditorsButton/styles/index.ts— 2 removed, 0 left.innerButton/output: any→CSSProperties/Record<string, CSSProperties>.gui/WindowCommandsAndDialogs/commands/gotoAnything.ts— 2 removed, 0 left. LocalPluginMenuIteminterface forPluginManager.menuItems().find(...).gui/WindowCommandsAndDialogs/commands/importFrom.ts— 2 removed, 0 left.errors: any[]→(string|Error)[];onProgress(status: any)→Record<string, unknown>.gui/hooks/useEffectDebugger.ts— 2 removed, 0 left.effectHook/dependencies/dependencyNamestyped asEffectCallback/unknown[]/string[]; reduce accum typed.gui/hooks/useImperativeHandlerDebugger.ts— 2 removed, 0 left. Made generic over T;ref: Ref<T>,effectHook: ()=> T, deps typed.gui/hooks/usePropsDebugger.ts— 2 removed, 0 left.props: any→Record<string, unknown>;dependencies: any[]→unknown[].gui/lib/SearchInput/SearchInput.tsx— 2 removed, 0 left.inputRef?: any→React.Ref<HTMLInputElement>;onChange(event: any)→React.ChangeEvent<HTMLInputElement>.gui/utils/loadScript.ts— 2 removed, 0 left.attrs?: Record<string, any>→Record<string, string>;element: any→ typedHTMLLinkElement | HTMLScriptElement | nullwith branch-specific locals.services/plugins/PlatformImplementation.ts— 2 removed, 0 left.Componentsindex →unknown;registerComponent(_, component: any)→unknown.services/plugins/UserWebview.tsx— 2 removed, 0 left.theme?: any→Record<string, unknown>;ref: any→React.Ref<UserWebviewRef>(new exported interface).gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.ts— 2 removed, 0 left.searchMarkers: any→SearchMarkers;webviewRef: RefObject<any>→RefObject<NoteViewerControl>.gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.ts— 2 removed, 0 left. LocalWebviewIpcEvent { channel?; args? }used for bothonMessageparameter and the returned callback;arg0castas numberat thepercentScrollcall.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useLineSorting.ts— 0 removed, 2 left. Same dynamic CM5 loader / runtime instance reason as the other v5 utils. Reasons updated.gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.ts— 0 removed, 2 left. TinyMCE editor + ToggleButton api types not in published types. Reasons updated.gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.ts— 1 removed, 1 left.scheduleOnScroll(event: any)→{ percent: number };editor: anystays (TinyMCE Editor type narrower than getDoc/getWin usage). Reason updated.gui/NoteEditor/utils/useFormNote.ts— 2 removed, 0 left.editorRef: any→RefObject<NoteBodyEditorRef>(exposed a floating Promise inhandleAutoFocus; fixed withvoid);onResourceChange(event: any)→{ id: string }.gui/NoteEditor/utils/useWindowCommandHandler.ts— 2 removed, 0 left.noteSearchBarRef: any→MutableRefObject<HTMLInputElement | null>;execute(..., ...args: any[])→unknown[].gui/NoteList/utils/types.ts— 2 removed, 1 left.themeId: any→number;resizableLayoutEventEmitter: any→EventEmitter;searches: any[]left with descriptive reason (matches lib reducer shape).gui/NoteListWrapper/NoteListWrapper.tsx— 1 removed, 0 left.resizableLayoutEventEmitter: any→EventEmitter;depNameToNoteProp(event.name as any)→as ListRendererDependency.gui/Sidebar/hooks/useOnRenderItem.tsx— 2 removed, 0 left.(folder as any).note_count→(folder as FolderEntity & { note_count?: number }).note_count(dynamic SQL-only field).
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 431 → 368 disable comments (63 removed; cumulative 109/477).
Checkpoint 6 (2026-05-13):
gui/WindowCommandsAndDialogs/commands/editAlarm.ts— 3 removed, 0 left.comp: any→WindowControl;onClose(answer: any, buttonType: string)→(unknown, unknown)withas numberfortodo_due;mapStateToTitle(state: any)→State(from lib reducer).gui/NoteListHeader/useDragAndDrop.ts— 3 removed, 0 left.setupDataTransfer.data: any→unknown;onResizerDragOver as any(×2) →as unknown as EventListenerforadd/removeEventListener.gui/NoteListItem/utils/prepareViewProps.ts— 3 removed, 0 left.output: any→ typed local shape{note?: {folder?}; item?: {size?; selected?; index?}};(note as any)[propName]/(itemSize as any)[propName]→as unknown as Record<string, unknown>.gui/ResizableLayout/utils/movements.ts— 3 removed, 0 left.array_movemade generic over T;produce(layout, (draft: any))→LayoutItem;newSize: any→{ width?: number; height?: number }.gui/ToolbarButton/ToolbarButton.tsx— 3 removed, 0 left.getProp(props, name, defaultValue: any = null)→unknowndefault; record-cast accessors; casttitle/tooltip/iconName as stringandonClick as (()=> void) | undefinedat call sites.InteropServiceHelper.ts— 1 removed, 2 left.Promise<any>→Promise<Buffer | null>(this exposed a downstreamwriteFile(string)mismatch inusePrintToCallback.ts— addedas unknown as stringthere with the runtime accepting Buffer). The other two (pageSize as any,webContents.print(options as any)) stay with descriptive reasons (Electron's option types stricter than what we pass).gui/MasterPasswordDialog/Dialog.tsx— 0 removed, 3 left. PasswordInput'sChangeEventHandleris typed as(event: {value: string})=> voidbut the runtime hands a DOMReact.ChangeEventthrough (StyledInput passes the raw onChange). Updated reason on all three handlers.gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx— 2 removed, 1 left.themeId: any→number;infoComp: any = null→React.ReactNode.onPasswordInputChangeleft for the same PasswordInput reason above; reason updated.gui/NoteListControls/NoteListControls.tsx— 3 removed, 0 left.StyleProps = any→ local interface;StyledRoot: anytyped via interface;iconMap: any→Record<string, string>.gui/PdfViewer.tsx— 3 removed, 0 left.StyleProps = any→ local theme interface;resource: any→ResourceEntity;onMessage_(event: any)→MessageEvent<{ name; text? }>.gui/ConfigScreen/controls/SettingComponent.tsx— 2 removed, 1 left.onChange(event: any)→{ value: unknown };inputStyle: any→CSSProperties. ThesettingKeyToControldeclaration'sReact.FC<any>left — each control's props differ and tightening would require structural changes. Reason updated.gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.tsx— 3 removed, 0 left.pluginAssets: any[]→RenderResultPluginAsset[];(commands as any)[cmd.name](cmd.value)→ typed record cast;options: any→ inline typed object with optionalpercent.gui/ResizableLayout/utils/persist.ts— 4 removed, 0 left.saveLayout(layout): any→Partial<LayoutItem>;produce(layout, (draft: any))→LayoutItem;delete (item as any)[k]→as unknown as Record<string, unknown>;loadLayout(layout: any)→Partial<LayoutItem> | nullwithas LayoutItemon the spread.gui/NoteEditor/utils/resourceHandling.ts— 4 removed, 0 left.commandAttachFileToBody.options→ localCommandAttachFileToBodyOptions;resourcesStatus.resourceInfos: any→ResourceInfos(renderer);clipboardImageToResource.image: any→NativeImage(electron type import);getResourcesFromPasteEvent.event: any→{ preventDefault } | null.gui/SearchBar/SearchBar.tsx— 4 removed, 0 left.inputRef?: any→MutableRefObject<HTMLInputElement | null>;onChange(event: any)→{ value: string };onKeyDown(event: any)→React.KeyboardEvent; innerdocument.activeElement as any→as HTMLElement.gui/WindowCommandsAndDialogs/commands/showPrompt.ts— 4 removed, 0 left.comp: any→WindowControl;value?: any→string(matches DialogState shape);autocomplete?: any[]→unknown[];onClose(answer: any, buttonType: string)→(unknown, unknown).gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts— 4 removed, 0 left.webviewRef: RefObject<any>→RefObject<NoteViewerControl>;insertText(value: any)→string;(editorRef.current as any)[value.name](×2) → singleeditorAsRecordalias typedRecord<string, (...args: unknown[])=> unknown>.bridge.ts— 3 removed, 1 left.showOpenDialog(this.lastSelectedPaths_ as any)[fileType](×2) → typedkeyof LastSelectedPath;shouldShowMenu(_event: any, params: any)→(unknown, { isEditable: boolean }). The remainingas anyondialog.showOpenDialog(this.activeWindow(), options as any)stays — ourOpenDialogOptions.propertiesisstring[]but Electron's is a strict union. Reason updated.gui/ConfigScreen/controls/plugins/PluginsStates.tsx— 4 removed, 0 left.styled.div<any>/styled(StyledMessage)<any>→ typed prop interfaces;value: any→SerializedPluginSettings;onSearchPluginSettingsChange(event: any)→OnPluginSettingChangeEvent.
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 368 → 312 disable comments (56 removed; cumulative 165/477).
Checkpoint 7 (2026-05-13):
gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchExtension.ts— 3 removed, 2 left.getSearchTerm.keyword: any→Keyword;match: any→{from: {line; ch}; to: {line; ch}}(CodeMirror 5 DocumentPosition);marks: any→ReturnType<typeof highlightSearch>[]. Thestream: anyoverlay token signature stays — CM5 StringStream isn't typed in this repo.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.ts— 0 removed, 5 left. AllCodeMirror: any/cm: any/params: anyreasons updated to mention the CM5 dynamic loader / no @types/codemirror.gui/ResourceScreen.tsx— 5 removed, 0 left.onResourceClick/Delete/ToggleSorting's returnany→void;rootStyle: any→CSSProperties & { height?; width? };mapStateToProps(state: any)→AppState.gui/WindowCommandsAndDialogs/utils/appDialogs.tsx— 0 removed, 5 left. Reasons consolidated — each dialog requires a differentcustomPropsshape and the render functions intentionally spread an open shape.app.reducer.ts— 5 removed, 1 left.watchedResources: any→Record<string, unknown>;navHistory: any[]→AppStateRoute[];createAppDefaultState.resourceEditWatcherDefaultState: any→Partial<AppState>(this required spreadingbackgroundWindows: {}to override the lib's looser default);getNextLayout.currentLayout: any→string | string[];(item as any)[propName]→as unknown as Record<string, unknown>. The main reduceraction: anystays with reason — would need a redux action union.gui/ShareFolderDialog/ShareFolderDialog.tsx— 6 removed, 0 left.styled(StyledMessage)<any>× 2 → typed prop interfaces (<{index: number}>and the bare form);StyleProps = any→ local interface;handleError(error: any)/defer(error: any)→Error/Error | null;recipientEmail_change(event: any)→React.ChangeEvent<HTMLInputElement>. Neededtype="info"on<StyledRecipient>and<StyledShareState>to satisfy the StyledMessage'stype: stringrequired prop that the original<any>cast had hidden (runtime branches ontype === 'error', so 'info' = default styling).gui/NoteEditor/NoteEditor.tsx— 6 removed, 0 left.onFieldChange.value: any→string;onTitleChange(event: any)→React.ChangeEvent<HTMLInputElement>;onBodyWillChange(event: any)→{ changeId: number };externalEditWatcher_noteChange(event: any)/onNotePropertyChange(event: any)→{ id; note: NoteEntity }/{ note: NoteEntity }(matches theAlarmChangeEventshape lib emits);(newFormNote as any)[key]→as unknown as Record<string, unknown>via a typednoteAsRecordalias.ElectronAppWrapper.ts— 7 removed, 0 left.stateOptions: any→ typed object literal;windowOptions: any→BrowserWindowConstructorOptions(uncovered thatenableRemoteModuleis no longer in the publishedWebPreferences; cast with comment explaining @electron/remote still relies on it);(event as any).isMainFrame→as Electron.Event & {isMainFrame?: boolean};close/open-urlevent handlers →import('electron').Event;ipcMain.onhandlers →import('electron').IpcMainEventwithargstypedunknownand cast at the assignment site. (Usedimport('electron').*inline rather thanElectron.*because the project's lint config doesn't surface theElectronglobal.)gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx— 4 removed, 3 left.commands: any→Record<string, (...args: any[])=> unknown>(kept inneranywith reason — commands are heterogeneous and dispatched by name);replaceSelection.value: any/insertText.value: any→string;onEditorPaste.event: any→{ preventDefault } | null;loadScript.script: any/element: any→{src; id?; attrs?}/HTMLScriptElement | HTMLLinkElementwith branch-specific narrowing;options: any→ typed object with optionalpercent. Two of the remaining disables collapsed into the newcommandstyping comment.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.ts— 0 removed, 7 left. All CM5 mode/state/streamanys reason-updated (no @types/codemirror in this repo).gui/NoteListItem/utils/useItemElement.ts— 7 removed, 0 left. Allas anyevent-listener casts →as unknown as EventListener; React→DOM event casts →as unknown as React.MouseEvent<HTMLDivElement>;(element.style as any)[n]→as unknown as Record<string, unknown>.
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 312 → 266 disable comments (46 removed; cumulative 211/477).
Checkpoint 8 (2026-05-13):
gui/WindowCommandsAndDialogs/commands/setTags.ts— 8 removed, 0 left.comp: any→WindowControl; introduced localTagOption { value; label }for the suggestion arrays; sort/map callbacks typed viaTagEntity;onClose(answer: any[])→unknownwith internalas TagOption[]. Thevalue: startTagsneededas unknown as stringbecause DialogState's value is typedstring.gui/NotePropertiesDialog.tsx— 7 removed, 2 left.okButton: any→RefObject<HTMLButtonElement>;styles_: any→Record<string, CSSProperties>;buttonRow_click(event: any)→{ buttonName: string };editPropertyButtonClick.initialValue: any→string | number | null;(newFormNote as any)[k]and(formNote as any)[key]→as unknown as Record<string, unknown>casts;editedValue: any→string | number | null. Two stay with descriptive reasons:latLongFromLocationoutput spreads into NoteEntity which expects number lat/long while the code keeps them as strings (runtime coercion);createNoteField.valueis genuinely heterogeneous (timestamps + ids + urls).gui/utils/NoteListUtils.ts— 0 removed, 11 left. AllcommandToStatefulMenuItem(...) as anycasts get a consolidated reason: lib's MenuItem shape doesn't structurally satisfy Electron's MenuItemConstructorOptions.app.ts— 4 removed, 6 left.shouldShowMenu(_event: any, params: any)→(unknown, { isEditable; inputFieldType });ResourceEditWatcher.on('resourceChange', event: any)→{ id: string };(window as any).joplin→as unknown as Record<string, unknown>. The reducer/middleware/Tesseract/menu callback/redux dispatchanys stay with descriptive reasons (base class signatures, dynamic loader, heterogeneous menu items).gui/ConfigScreen/ConfigScreen.tsx— 8 removed, 2 left. Class declaration<any, any>stays with reason (legacy class component);private rowStyle_: any→React.CSSProperties;sidebar_selectionChange.event: any/renderSectionDescription.section: any/sectionToComponent.section: any/settings: any→SettingMetadataSection+Record<string, unknown>;sectionWidths: Record<string, any>→Record<string, string>;sectionStyle: any→React.CSSProperties;needRestartComp: any→React.ReactNode;mapStateToProps(state: any)→AppState. Requiredsettings['sync.target'] as numbercasts at call sites. Constructorprops: anystays (matches the class's open props type).services/plugins/PluginRunner.ts— 11 removed, 0 left.ipcRendererSend.args/eventHandler.args→unknown/unknown[];PluginMessage.args/result/error→unknown[]/unknown; introduced localCallbackPromiseinterface and typedcallbackPromises: Record<string, CallbackPromise>;mapEventIdsToHandlers.arg: any→unknownwith internalas Record<string, unknown>for the object-iteration branch;ipcRenderer.onhandler typed viaIpcRendererEvent;result/errorlocals →unknown/Error | nullwithas Errorin the catch.gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.ts— 3 removed, 7 left.scrollTimeoutId_: any/restoreEditorPercentScrollTimeoutId_: any→ReturnType<typeof setTimeout> | null;scheduleOnScroll.event: any→{ percent: number }. The remaining seven CM5cm/codeMirror: anyentries reason-updated to mention the dynamic editor/scrollInfo types.gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.ts— 0 removed, 9 left. All CM5 plugin-loaderanys reason-updated.
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 266 → 226 disable comments (40 removed; cumulative 251/477).
Checkpoint 9 (2026-05-13):
gui/MainScreen.tsx— 12 removed, 4 left.style: any→CSSProperties & { width?; height? };StatefieldspromptOptions/notePropertiesDialogOptions/noteContentPropertiesDialogOptions/shareNoteDialogOptions→Record<string, unknown>;waitForNotesSavedIID_: any→ReturnType<typeof setInterval>;ipcRenderer.onhandler →(IpcRendererEvent, string, { url });produce(layout, (draft: any))→LayoutItem;layoutModeListenerKeyDown.event: any→KeyboardEvent;urlStyle: any→React.CSSProperties;renderNotification.styles: any→Record<string, CSSProperties>;resizableLayout_resize.event: any→{ layout: LayoutItem };resizableLayout_renderItem.event: any→ typed shape (eventEmitter: EventEmitter; visible; size: Size; item: LayoutItem) — also added missingreturn null;and the explicit: React.ReactNodereturn type;components: any→Record<string, ()=> React.ReactNode>;dispatch as any→as unknown as Dispatch. Left:styles_: any(heterogeneous — CSSProperties blocks + computed numbers like rowHeight); reducer/middleware-like patterns elsewhere.gui/NoteEditor/NoteBody/CodeMirror/v5/Editor.tsx— 0 removed, 17 left. All CM5 dynamic editor/event/optionsanys reason-updated; coversEditorPropsheterogeneous callbacks (onChange/onScroll/onEditorPaste/onResize/onUpdate), the dynamiccmOptionsrecord, and ref/wrapper signatures.gui/NoteEditor/utils/types.ts— 12 removed, 5 left.NoteEditorProps:notes: any[]→NoteEntity[];editorNoteStatuses: any→Record<string, string>;selectedNoteTags: any[]→TagEntity[];watchedResources: any→Record<string, unknown>;highlightedWords: any[]→string[].NoteBodyEditorProps:style: any→React.CSSProperties;onWillChange.event: any→{ changeId: number };noteToolbar: any→React.ReactNode; danglingsearchMarkers: anydisable removed (already typedSearchMarkers).MessageEvent.args: any[]reason updated.bodyEditorContent: anyreason updated (TinyMCE retains a raw editor object).ScrollOptions.value/OnChangeEvent.content/EditorCommand.value/CommandValue.args/valuestay with descriptive reasons — each editor dispatches heterogeneous shapes through these fields; tightening would require per-command discriminated unions across all editors.searches: any[]stays (matches lib reducer).
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 226 → 202 disable comments (24 removed; cumulative 275/477).
Checkpoint 10 (2026-05-13):
gui/PromptDialog.tsx— 11 removed, 9 left.defaultValue/answer/autocomplete/buttons: anystay with descriptive reasons (DialogState.promptOptions has heterogeneous values per inputType).styles_: anystays (heterogeneous style blocks + react-select factories).answerInput_: anystays (HTMLInputElement vs react-select ref depending on inputType). All six react-select style/theme factory callbacks (control/input/menu/option/multiValueLabel/multiValueRemove/selectTheme) reason-updated.onSelectChange.newValue: any→unknown;onKeyDown.event: any→React.KeyboardEvent.makeAnimated() as anycasts kept inline with reasons.gui/plugins/GotoAnything.tsx— 13 removed, 7 left.UserDataCallbackEvent.item: any→NoteEntity | FolderEntity | ResourceEntity | TagEntity;folders: any[]→FolderEntity[];Dialog: any→React.ComponentType<Props>;manifest: any→ typed object with menuItem shape;onTrigger.event: any→{ userData };inputRef/itemListRef: any→RefObject<HTMLInputElement>/RefObject<ItemList<...>>;input_onChange/listItem_onClick/input_onKeyDown.event: any→ React event types;results: any[]/row: any/result: any→GotoAnythingSearchResult[]andresult.idaccesses;mergeOverlappingIntervals.f: any→[number, number];gotoItem.item: any→GotoAnythingSearchResult & { commandArgs? };selectedItemIndex.results: any[]→GotoAnythingSearchResult[]. Notes results fromTag.searchAllWithNotes/SearchEngine.searchand thefolderspread needas unknown as GotoAnythingSearchResult[]casts since lib's signatures are looser.styles_: anyand the heterogeneouscommandResultscallback signature stay.gui/MenuBar.tsx— 0 removed, 23 left. All 23anyusages reason-updated: ElectronMenuItemConstructorOptionshas heterogeneous shapes (submenu/role/type/click vary by item kind) and the menu structure is built dynamically.
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 202 → 179 disable comments (23 removed; cumulative 298/477).
Checkpoint 11 (2026-05-13) — final:
gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx— 2 removed, 18 left.stripMarkup.options: any→{ collapseWhiteSpaces?: boolean };dispatchDidUpdateIID_: any→ReturnType<typeof setTimeout> | null. All 18 remaininganys reason-updated to mention TinyMCE editor instance/event types being looser than @types/tinymce (we use APIs like getDoc/getWin/formatter/ui.registry/undoManager extensions that aren't in the published types).gui/MenuBar.tsx— already updated reasons in checkpoint 10.gui/NoteEditor/NoteBody/CodeMirror/utils/useContextMenu.ts— 0 removed, 1 left. Combinedgithub/array-foreach+no-explicit-anydisable kept on one line (lint requires consecutivedisable-next-linedirectives to be merged); reason updated to mention lib's MenuItem shape vs Electron's MenuItemConstructorOptions.gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.ts— already in checkpoint 8. Inline disable consolidated.gui/NoteEditor/utils/useSearchMarkers.ts— 0 removed, 1 left. Combinedban-types+no-explicit-anydisable kept on one line; reason explainssearches: any[]matches lib reducer andhighlightedWordsis heterogeneous (string[] at call site, keyword shapes inside).
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean on changed files; spellcheck clean. 179 → 177 disable comments (2 removed; cumulative 300/477 — 63% reduction).
Summary
app-desktop final state: 177 disable comments remaining out of 477 (300 removed, 63% reduction).
All 177 remaining disables now have descriptive -- reason comments explaining why they can't be tightened. They fall into these categories:
- CodeMirror 5 dynamic loader / dynamic editor / scrollInfo / line-handle types — no
@types/codemirrorin this monorepo. Files:useJoplinMode.ts(7),useCursorUtils.ts(5),useListIdent.ts(9),useScrollHandler.ts(7),v5/CodeMirror.tsx(3),Editor.tsx(17), and several smaller utils. - TinyMCE editor / event types — looser than
@types/tinymce(we use APIs not in the published types). File:TinyMCE.tsx(18). - Electron MenuItemConstructorOptions — heterogeneous shapes (submenu/role/type/click vary by item kind). Files:
MenuBar.tsx(23),NoteListUtils.ts(11),app.ts(a few). - react-select style/theme factories — library's own provided styles, tightening requires importing each StyleConfig generic. File:
PromptDialog.tsx(9). - Redux actions / middleware — heterogeneous action types; tightening would require an action-type union and base class signature change. Files:
app.ts,app.reducer.ts,MainScreen.tsx. - Legacy class components without props/state interfaces —
Root.tsx,OneDriveLoginScreen.tsx,DropboxLoginScreen.tsx,ConfigScreen.tsx,PromptDialog.tsx. - Heterogeneous editor commands —
EditorCommand.value/CommandValue.args/value/ScrollOptions.value/OnChangeEvent.content— each editor dispatches different shapes by name. - Library API mismatches — Electron's
@electron/notarizetypes missingappBundleId;WebPreferences.enableRemoteModuleremoved; Electron'sOpenDialogOptions.propertiesis a strict union but the app usesstring[]. - Heterogeneous test fixtures / external library shapes —
electron-context-menu's actions/props, PluginManager dynamic menu items,tesseract.jsdynamic loader. - CSS / styling —
styled-components.attrstyping conflicts;styles_blocks that mix CSSProperties with computed numbers.
packages/app-cli
Session date: 2026-05-13
Files processed:
app/command-cp.ts— 1 removed, 0 left. Typedaction({ note, notebook? }).app/command-mv.ts— 1 removed, 0 left. Typedaction({ item, notebook }).app/command-ren.ts— 1 removed, 0 left. Typedaction({ item, name }).app/command-rmbook.ts— 1 removed, 0 left. Typedaction({ notebook, options? }).app/command-rmnote.ts— 1 removed, 0 left. Typedaction({ 'note-pattern', options? }).app/command-restore.ts— 1 removed, 0 left. Typedaction({ pattern }).app/command-edit.ts— 1 removed, 0 left. Typedaction({ note }).app/command-import.ts— 1 removed, 0 left. Typedaction({ path, notebook?, options }); outputFormat assignment now requiresas ImportModuleOutputFormat.app/command-mkbook.ts— 1 removed, 0 left. Typedaction({ 'new-notebook', options }).app/command-help.ts— 1 removed, 0 left. Typedaction({ command? }).app/command-use.ts— 1 removed, 0 left. Typedaction({ notebook }).app/command-attach.ts— 1 removed, 0 left. Typedaction({ note, file }).app/command-cat.ts— 1 removed, 0 left. Typedaction({ note, options }).app/command-geoloc.ts— 1 removed, 0 left. Typedaction({ note }).app/command-apidoc.ts— 1 removed, 0 left. Typedaction({ file }).app/command-done.ts— 2 removed, 0 left. TypedhandleAction(args: { note })andaction(args: { note }).app/command-set.ts— 2 removed, 0 left. Typedaction(args: { note, name, value? });newNote: any→Record<string, unknown>(still accepted byNote.save).app/command-ls.ts— 1 removed, 1 left. Typedactionargs; leftqueryOptions: anybecause it is fed to bothFolder.all(FolderLoadOptions) andNote.previews(PreviewsOptions) and the union has fields that neither type carries (caseInsensitive,orderBy). Comment reason updated.app/setupCommand.ts— 2 removed, 0 left. Typedcmd: BaseCommand, introduced localPromptOptions; the dispatcher arg is inferred fromBaseCommand.setDispatcher(fn: DispatcherFn)(added in base-command.ts edit), eliminating the lastanyhere. Side fix:App.setupCommand(cmd: string)was a wrong-pre-existing-annotation, now typedBaseCommand.app/command-e2ee.ts— 2 removed, 0 left. Typedactionargs; typedaskForMasterKey(error: { masterKeyId }).app/command-config.ts— 3 removed, 0 left.chunks: any→Buffer[]; typed action args;Record<string, any>→Record<string, unknown>for resultObj.app/command-export.ts— 3 removed, 0 left. Typed action args;n.idmap callbacks now infer entity types fromloadItems; format assignment usesExportModuleOutputFormat.Jex.app/gui/FolderListWidget.ts— 3 removed, 0 left. Introduced localFolderListItem = FolderEntity | TagEntity | SearchItem | '-'foritemRendererandnewItems; replaced(this.folders[i] as any).note_countwithFolderEntity & { note_count?: number }.app/command-settingschema.ts— 4 removed, 0 left. Typed action args;Record<string, any>→Record<string, unknown>for schema, props; removed unusedv: anyannotation.app/gui/StatusBarWidget.ts— 2 removed, 2 left. Typedprompt(promptString: string, options)andtextStyle: (s: string) => s. Left 2anyfor tkwidgets terminal-kitinputFieldoptions/callback — no published types and tkwidgets has no .d.ts. Comments updated with reason.app/LinkSelector.ts— 5 removed, 0 left. Introduced localTextWidgetinterface for tk text widget shape; replaced(lines[i] as any).matchAllwith direct call (String.prototype.matchAllis well-typed).app/command-sync.ts— 3 removed, 2 left. Typedactionargs,log(...s: string[]). Leftoptions: anyandreport: anybecauseSynchronizer.start(options: any)andSynchronizer.reportToLines(report: any)in lib are themselvesany— tightening here would diverge from lib. Comments updated.app/command-testing.ts— 5 removed, 0 left.randomElement→ generic<T>(array: T[]): T | null;itemCount(args: { arg0 });options(): [string, string][]; typedactionargs;promises: Promise<unknown>[].app/app.ts— 0 removed, 6 left. All disables had descriptive reasons (Dynamic command loading system,Dynamic command metadata,Dynamic command type,Dynamic GUI type with many optional methods,Redux dispatch type requires AnyAction) — none wereOld code before rule was applied, so out of scope per rule 3. Side fix in checkpoint 1:App.setupCommand(cmd: string)annotation corrected toBaseCommand.app/services/plugins/PluginRunner.ts— 6 removed, 0 left.wrapper: any→Record<string, (...args: unknown[]) => unknown>with final cast totypeof Consoleto matchSandboxProxy.console; arg arrays →unknown[];activeSandboxCalls_: any→Record<string, boolean>.app/base-command.ts— 4 removed, 4 left. Introduced typedStdoutFn/PromptFn/DispatcherFnaliases; typedstdout_/prompt_/dispatcher_fields; typedencryptionCheck(item: { encryption_applied? }). Leftaction(_args: any)andoptions(): unknown[]: parameters are contravariant so tightening base would break all subclass overrides. The 3 remaininganyare now centralized in the type aliases at the top of the file (with reasons): stdout accepts arbitrary message values, prompt response varies, dispatch action shape varies; tests pass sync mocks that don't return Promises.tests/HtmlToMd.ts— 1 removed, 0 left.htmlToMdOptions: any→ParseOptions(exported from@joplin/lib/HtmlToMd).tests/MdToHtml.ts— 3 removed, 0 left.newTestMdToHtml(options: any)→Partial<MdToHtmlConstructorOptions>; theResourceModelmock requiresas unknown ascast because it intentionally omitsfilenameandisSupportedImageMimeType.mdToHtmlOptionsalready had a real type, just a stale disable comment.pluginOptions: any→Record<string, { enabled: boolean }>.tests/testUtils.ts— 1 removed, 0 left.Record<string, any>→Record<string, unknown>onPluginServiceOptions.getState.tests/services/keychain/KeychainService.ts— 1 removed, 0 left.describeIfCompatible(fn: any, elseFn: any)→() => void.tests/services/plugins/PluginService.ts— 1 removed, 0 left.(f: any) => f.title→ inferredFolderEntity(fromFolder.all()).tests/services/plugins/api/JoplinWorkspace.ts— 2 removed, 0 left.appState: Record<string, any>→Record<string, string[]>;result: any→{ id: string; event: number }.tests/services/plugins/sandboxProxy.ts— 4 removed, 0 left.args: any[]→unknown[];target: any→ inferred function type (bothitblocks).
Verification: package yarn tsc --noEmit clean; yarn linter-ci packages/app-cli/ clean; root yarn tsc --noEmit (all workspaces) clean; spellcheck clean.
Summary: 90 → 16 disable comments (74 removed). Zero remaining Old code before rule was applied disables — all 16 left have descriptive reasons explaining why they cannot be tightened. They fall into these categories:
- Base-class contravariance (4):
base-command.ts—StdoutFn,PromptFn,DispatcherFnaliases at the top of the file (centralized) and theaction(_args: any)method. Parameters are contravariant; subclasses narrow per-command, so the base must stay permissive. Tests pass synchronous mocks that don't return Promises. - Heterogeneous query options (1):
command-ls.tsqueryOptionsis passed to bothFolder.all(FolderLoadOptions) andNote.previews(PreviewsOptions) and the union has fields neither type carries; splitting would be a structural refactor. - Loose lib-typed APIs (2):
command-sync.ts—Synchronizer.start(options: any)andSynchronizer.reportToLines(report: any)in lib are themselvesany; tightening here would diverge from lib. - No published types for JS modules (3):
StatusBarWidget(tkwidgets terminal-kitinputFieldoptions + callback) andcommand-sync.tsoneDriveApiUtils_(onedrive-api-node-utils.jsis plain JS). - Dynamic command/state shapes already documented (6): all of
app.tswas already annotated with descriptive non-"Old code" reasons before this pass —commands_,commandMetadata_,activeCommand_,gui_, dynamic command type at L175, and the redux dispatch type.
packages/lib
Session date: 2026-05-14
Starting count: 1138 disable comments across 212 files (excluding node_modules/).
Checkpoint 1 (2026-05-14): 1138 → 1101 (37 removed across ~25 files).
Files processed:
SyncTargetNone.ts— 1 removed, 0 left.null as any→null as unknown as Synchronizerwith type-only import.components/shared/config/shouldShowMissingPasswordWarning.ts— 1 removed, 0 left.settings: any→Record<string, unknown>.folders-screen-utils.ts— 1 removed, 0 left.scheduleRefreshFoldersIID_: any→ReturnType<typeof shim.setTimeout>.geolocation-node.ts— 1 removed, 0 left.fetchJsonreturn →Record<string, unknown>(and renamed shadowedlet rtoconst responseto keep types straight after.json()).hooks/useAsyncEffect.ts— 1 removed, 0 left.dependencies: any[]→DependencyList(type-only import from react).hooks/useElementSize.ts— 1 removed, 0 left.elementRef: any→RefObject<HTMLElement>(type-only import from react).markdownUtils.ts— 1 removed, 0 left.searchUrls(tokens: any[])→MarkdownItType.Token[](already had type-only import).markupLanguageUtils.ts— 1 removed, 0 left.pluginOptions: any→Record<string, { enabled: boolean }>.models/Note.test.ts— 1 removed, 0 left.t[0]castas NoteEntityinstead of innerlet input: any.models/NoteResource.ts— 1 removed, 0 left. Introduced exportedAssociatedResourceNote = Partial<NoteEntity> & { resource_id; note_id }to reflect the join shape.models/Revision.test.ts— 1 removed, 0 left.input as any→input as RevisionEntity.models/Revision.ts— 0 removed, 1 left. Triedunknown[]for parsePatch; brokepatchItem.diffsaccess. Reverted with updated reason — diff-match-patch JSON shape, no installed@types/diff-match-patch.models/Setting.test.ts— 1 removed, 0 left.loadSettingsFromFile(): Promise<any>→Promise<Record<string, unknown>>.models/settings/FileHandler.ts— 1 removed, 0 left.SettingValues = Record<string, any>→Record<string, unknown>.ntp.ts— 1 removed, 0 left.error: any→Error | nullin the NTP callback signature.services/ResourceEditWatcher/reducer.ts— 0 removed, 1 left. Reason updated (heterogeneous redux state composed across desktop/mobile; action types untyped).services/database/isSqliteSyntaxError.ts— 1 removed, 0 left.sqliteError: any→{ message?: string }.services/interop/InteropService_Importer_EnexToMd.ts— 1 removed, 0 left.options: any→ImportOptions.services/interop/InteropService_Importer_Raw.test.ts— 1 removed, 0 left. Introduced localFolderEntityWithChildreninterface;tree: any→ typed cast.services/interop/Module.test.ts— 1 removed, 0 left.format: ... as any→as ExportModuleOutputFormat.services/interop/Module.ts— 1 removed, 0 left.format: '' as any→as ExportModuleOutputFormat.services/joplinCloudUtils.ts— 1 removed, 0 left.Action.payload?: any→string(only ever holds errorMessage).services/noteList/defaultMultiColumnsRenderer.ts— 0 removed, 1 left. Reason updated (matches OnRenderNoteHandler which isanyby design; props heterogeneous per renderer's itemProps).services/noteList/renderTemplate.test.ts— 1 removed, 0 left.name: '…' as any→as ColumnName.services/noteList/renderViewProps.ts— 1 removed, 0 left.value: any→unknownwith inner narrowing casts.services/ocr/OcrService.ts— 1 removed, 0 left.maintenanceTimer_: any→ReturnType<typeof shim.setInterval>.services/plugins/MenuController.ts— 1 removed, 0 left.store: any→PluginStore(new exported alias fromViewController).services/plugins/MenuItemController.ts— 1 removed, 0 left. Same fix viaPluginStore.services/plugins/ToolbarButtonController.ts— 1 removed, 0 left. Same fix viaPluginStore.services/plugins/ViewController.ts— 3 removed, 1 left. Introduced exportedPluginStore = Store<any>(state heterogeneous across desktop/mobile —mainLayoutlives on AppState only); typedstore_/store/message.storeViewstaysany(controllers index different view shapes).services/plugins/Plugin.ts— 4 removed, 0 left. Introduced exportedMessageListenerCallback = (message: unknown)=> Promise<unknown>; typedmessageListener_,contentScriptMessageListeners_,emitMessage,onMessage,onContentScriptMessage,emitContentScriptMessage.services/plugins/RepositoryApi.ts— 1 removed, 0 left.(manifest as any)[field]→as consttuple + direct indexing.services/plugins/api/JoplinContentScripts.ts— 1 removed, 0 left.callback: any→MessageListenerCallback.services/plugins/api/JoplinInterop.ts— 1 removed, 0 left....module as any→ spread +format: module.format as ExportModuleOutputFormat.services/plugins/utils/loadContentScripts.ts— 1 removed, 1 left.postMessageHandlernow async returningPromise<unknown>. TheloadedModule.codeMirrorResources/codeMirrorOptionsaccess keptas anywith updated reason — these properties are not onContentScriptModule.services/interop/InteropService_Importer_EnexToHtml.ts— follow-up fix:outputFormat: 'html'→ImportModuleOutputFormat.Html(caused by ImportOptions tightening).
Files skipped entirely:
database-driver.ts—SelectResult = anyalready tagged "Partial refactor"; out of scope.file-api-driver-local.ts— already tagged "Partial refactor".services/interop/InteropService_Importer_Md_frontmatter.ts—parseRawYamlToFolderIconalready tagged "The raw YAML output is untyped".services/e2ee/ppk/RSA.node.ts— already tagged "Workaround for incorrect types".services/e2ee/types.ts— already tagged "Partial refactor".services/commands/ToolbarButtonUtils.ts— already tagged "WhenClauseContext can be partial in tests".
Checkpoint 2 (2026-05-14): 1101 → 1053 (48 removed across ~25 files).
services/plugins/api/JoplinPlugins.ts— 1 removed, 0 left.require(_path): any→unknown(stub).services/plugins/utils/manifestFromObject.ts— 1 removed, 0 left.(o: any)→Record<string, unknown>; PluginService.validateManifest now casts on the caller side.services/plugins/utils/mapEventHandlersToIds.ts— 0 removed, 1 left. Reason updated (recursive walker; tightening tounknownforces narrowing at every branch and recursive call).services/plugins/utils/validatePluginPlatforms.test.ts— 1 removed, 0 left.platforms: any→unknown; inner cast in call.services/profileConfig/index.ts— 1 removed, 0 left. Introduced localMigratingProfile/MigratingProfileConfiginterfaces describing the v1→v2 transition.services/rest/ApiResponse.ts— 1 removed, 0 left.body: any→unknown.services/rest/routes/auth.ts— 1 removed, 0 left.output: any→ inline{ status: AuthTokenStatus; token?: string }.services/rest/routes/search.ts— 1 removed, 0 left. Restructured to construct the options as a typed object literal; NotesForQueryOptions branch narrows the LoadOptions.fields union to string[].services/rest/utils/defaultAction.ts— 1 removed, 0 left.getOneModel.options: any→LoadOptions.services/rest/utils/defaultLoadOptions.ts— 1 removed, 0 left. Return →LoadOptions.services/rest/utils/defaultSaveOptions.ts— 1 removed, 0 left. Introduced exportedDefaultSaveOptions { userSideValidation; isNew?; autoTimestamp? }; callers setautoTimestampafter construction.services/search/SearchFilter.test.ts— 1 removed, 0 left.let engine: any→SearchEngine.services/sortOrder/PerFolderSortOrderService.ts— 1 removed, 0 left.event: any→{ value: string }.services/style/loadCssToTheme.ts— 1 removed, 0 left. Removed(f: any)annotation — readDirStats returns typed Stat[].services/synchronizer/ItemUploader.ts— 1 removed, 0 left.preUploadedItems_: Record<string, any>→Record<string, { error?: { message?; code? } }>.services/synchronizer/MigrationHandler.ts— 1 removed, 0 left.autoLockError: any/error: any→Error | null.services/synchronizer/gui/useSyncTargetUpgrade.ts— 1 removed, 0 left.error: any→Error | null.services/synchronizer/migrations/1.tsand2.ts— 2 removed, 0 left.api: any→FileApi.services/synchronizer/utils/handleConflictAction.ts— 1 removed, 0 left.remoteContent/local: any→BaseItemEntity.utils/ipc/types.ts— 0 removed, 1 left. Reason updated (structural constraint can't express "args extend SerializableData[]" without index-signature errors).utils/ipc/utils/mergeCallbacksAndSerializable.ts— 1 removed, 0 left.OnAfterCallbackCreatedcallback typed properly.utils/ipc/utils/separateCallbacksFromSerializable.test.ts— 1 removed, 0 left.as any[]→as string[].file-api-driver.test.ts— 1 removed, 0 left. Removed(f: any)from map; items typed already.file-api.test.ts— 1 removed, 0 left.syncContext: any→ inferred from literal withnull as unknownfor cache fields.ArrayUtils.ts— 2 removed, 0 left.mergeOverlappingIntervalstyped[number, number][]; one caller (GotoAnything.tsx) annotated itsindicesaccordingly.JoplinError.ts— 2 removed, 0 left. Introduced exportedJoplinErrorCode = string | number | null.ObjectUtils.ts— 2 removed, 0 left.output: anyinsortByValueandconvertValuesToFunctions→Record<string, ...>with final cast to the typed return type.dom.ts— 2 removed, 0 left.isInsideContainer(node: any)→EventTarget | Node | null;waitForElementmade generic<T extends HTMLElement>returningT | null. One caller (useRootElement.ts) now uses the generic explicitly.errorUtils.ts— 2 removed, 0 left. Introduced localWrapErrorInputandWrappedErrorinterfaces.net-utils.ts— 2 removed, 0 left. IntroducedTcpPortUsed { check(port): Promise<boolean> };Record<string, any>→Record<string, string>for headers.models/utils/types.ts— 2 removed, 0 left.LoadOptions.whereParams: any[]→(string|number|boolean)[];SaveOptions.oldItem: any→Record<string, unknown>; addedSaveOptions.fields?: string[](used byBaseModel.saveand at least one renameTag caller).models/Alarm.ts— 2 removed, 0 left.selectAllcast to{ id: string }[];makeNotification(alarm, note)→AlarmEntity/NoteEntity.models/Tag.ts— 2 removed, 0 left.searchAllWithNotes(options: any)→SearchOptions(new exported interface inBaseModel);save.options: any→SaveOptions.models/utils/readOnly.ts— 2 removed, 0 left.Folder: any→typeof import('../Folder').default;BaseItem: any→typeof import('../BaseItem').default.components/shared/config/config-shared.ts— 1 removed, 1 left.updateSettingValue.value: any→unknown. ThesetStatefield is leftanywith a new reason — mirrorsReact.Component.setState(Pick<S, K>); narrowing breaks subclassthisassignment to the interface in app-mobile's class-based ConfigScreen.components/shared/config/plugins/useOnInstallHandler.ts— 2 removed, 0 left. BothsetInstallingPluginIds((prev: any))callbacks rely on theReact.Dispatch<SetStateAction<...>>inferred type.components/shared/reduxSharedMiddleware.ts— 1 removed, 1 left.sortNoteListTimeout: any→ReturnType<typeof shim.setTimeout>;store/_nexttyped (Store<State>/Dispatch);action: anykept with new reason explaining the heterogeneous action union.- New shared types:
BaseModel.SearchOptions;JoplinError.JoplinErrorCode;models/NoteResource.AssociatedResourceNote;services/plugins/ViewController.PluginStore;services/plugins/Plugin.MessageListenerCallback;services/rest/utils/defaultSaveOptions.DefaultSaveOptions.
Follow-up edits in other packages (caused by lib tightenings):
app-desktop/gui/NoteListItem/utils/useRootElement.ts— callwaitForElement<HTMLDivElement>(...)explicitly.app-desktop/plugins/GotoAnything.tsx— annotated localindices: [number, number][].
Checkpoint 3 (2026-05-14): 1053 → 1004 (49 removed).
Plugin-API files (mostly thanks to the new PluginStore / MessageListenerCallback aliases from checkpoint 1):
services/plugins/api/JoplinClipboard.ts— 3 removed, 0 left. LocalElectronClipboardLike/ElectronNativeImageLikeinterfaces (electron module not available in non-desktop packages).services/plugins/api/JoplinViews.ts— 3 removed, 0 left.implementationnowBasePlatformImplementation.JoplinViews(the actual sub-object passed in);store→PluginStore.services/plugins/api/JoplinViewsDialogs.ts— 3 removed, 0 left. IntroducedShowOpenDialogOptionsinBasePlatformImplementation; tightened return tostring[] | null.services/plugins/api/JoplinViewsEditor.ts— 4 removed, 0 left.store→PluginStore;onMessage.callback: Function→MessageListenerCallback;postMessage.message: any→unknown.services/plugins/api/JoplinViewsPanels.ts— 3 removed, 0 left. Same pattern (PluginStore/MessageListenerCallback/unknown).services/plugins/api/JoplinViewsToolbarButtons.ts— 3 removed, 0 left.store→PluginStore; deprecation cast typed.services/plugins/api/JoplinViewsMenus.ts— 4 removed, 0 left.store→PluginStore; deprecation casts typed.services/plugins/api/JoplinViewsMenuItems.ts— 4 removed, 0 left. Same.services/plugins/api/JoplinSettings.ts— 4 removed, 0 left. All four setting-value returns/inputs →unknown.services/plugins/api/JoplinPlugins.ts— already in batch 2.services/plugins/api/Joplin.ts— follow-up: castimplementation.clipboard/.nativeImageto the JoplinClipboard constructor parameter shapes.services/plugins/BasePlatformImplementation.ts— 4 removed, 0 left. IntroducedShowOpenDialogOptions;clipboard/nativeImage/registerComponentreturns/params →unknown.services/plugins/api/noteListType.ts— 1 removed, 2 left.OnChangeEvent.value: any→unknown.RenderNoteViewandOnRenderNoteHandler.propskeptanywith updated reasons (heterogeneous per-renderer shape).
Top-level lib files:
eventManager.ts— 0 removed, 3 left. Reason updated onfilterEmit.object: any(filter objects vary per filter name). TwoPartial refactorreasons already in scope; no change.hooks/useEventListener.ts— 4 removed, 0 left. Introduced localEventHandler = (event: Event) => void; typedelementasRefObject<EventTarget | null>(type-only import from react).services/AlarmService.ts— 3 removed, 0 left. Introduced exportedAlarmServiceDriverinterface;updateNoteNotification.noteOrIdtypedNoteEntity | string.models/Alarm.ts— follow-up:selectAllcast updated to{ id: number }[](alarm IDs are integers, unlike note IDs);batchDeletecall now casts the resultingnumber[](the function's signature acceptsstring[]for note-style IDs).services/KvStore.ts— 4 removed, 0 left. IntroducedJoplinDatabaseandMutexInterfaceimports for typed fields;formatValues_callers cast theRow[]results toKvStoreKeyValue[].
Checkpoint 4 (2026-05-14): 1004 → 977 (27 removed).
TaskQueue.ts— 3 removed, 0 left.TaskCallback/TaskResult.result/completeTask.result→unknown.HtmlToMd.ts— 3 removed, 0 left.turndownOpts: any→Record<string, unknown>;blankReplacement/replacementcallbacks typed withHTMLElement.InMemoryCache.ts— 3 removed, 0 left.Record.value/value()/setValue()→unknown.SyncTargetOneDrive.ts— 2 removed, 1 left.api_: any→OneDriveApi;(a: any)→unknown. Left the inheriteddb/optionsconstructor params (BaseSyncTarget still usesanythere).htmlUtils.ts— 3 removed, 5 left (4 of the 5 are existingban-typesFunction disables and now-typed callbacks via newReplaceUrlCallback/ProcessImageTagCallbackaliases).attributesHtml.attr: any→Record<string, string>;headAndBodyHtml.doc: any→Document. One test (htmlUtils2.test.ts) gets a typed cast. TheFunction-typedprocessImageTags/replaceImageUrls/replaceEmbedUrls/replaceMediaUrlscallbacks are now typed via new aliases — the ban-types disables go away with the explicit-any ones.downloadController.ts— 3 removed, 0 left. Introduced localDownloadRequest/DownloadChunkinterfaces.services/interop/InteropService_Exporter_Base.ts— 3 removed, 1 left.prepareForProcessingItemType.itemsToExport: any[]→BaseItemEntity[];processItem.item: any→BaseItemEntity;processResource.resource: any→ResourceEntity;updateContext.context: any→object. Thecontext_field staysanywith updated reason — shape is exporter-specific (Html hascssStrings/customAssets, Md hasnoteTags/tagTitles) and indexed dynamically by subclasses.services/interop/InteropService_Exporter_Jex.ts— 3 removed, 0 left.processItem/processResourcenow match the tightened base;readDirStats.filter/mapcallbacks no longer need a cast.services/interop/InteropService_Exporter_Html.ts— 3 removed, 0 left.style_: any→ThemeStyle;init.options: any→ExportOptions;processItem.item: any→NoteEntity.
Follow-up: htmlUtils2.test.ts typed cast; app-desktop/.../resourceHandling.ts doesn't need changes (return type of its replaceImageUrls callback is void, which ReplaceUrlCallback now allows).
Checkpoint 5 (2026-05-14): 977 → 965 (12 removed across a handful of larger files).
services/share/ShareService.ts— 4 removed, 0 left.formatShareInvitations.invitations: any[]→ typed shape usingShareInvitation/MasterKeyEntity;store_: Store<any>→Store<unknown>;stategetter narrows viaas Record<string, unknown>.services/ExternalEditWatcher.ts— 5 removed, 3 left. Introduced localDispatchFn/BridgeFn;eventEmitter_typed viaEventEmitter(switched to ES import).chokidar_/watcher_stayanywith a single shared reason (chokidar typings vary across platforms; we use a small subset structurally).on/offcallbacks stayanywith reason (EventEmitter payloads vary per event name; per-event union would require touching every caller).theme.ts— 2 removed, 1 left.cachedStyles_shape typed (themeId, indexedstylesrecord);buildStyle.cacheKey: any→string | (string | number)[].BuildStyleCallbackreturn staysany(heterogeneous: CSSProperties, styled-components objects, plain CSS strings).services/share/reducer.ts— 2 removed, 1 left.parseShareCache.raw: any→Partial<State>. The reducer'saction: anykeeps a reason about heterogeneous SHARE_* action shapes.(draft.shareUsers as any)cast removed.services/interop/types.ts— 2 removed, 1 left.ImportOptions.destinationFolder: any→FolderEntity;onError: (error: any)→Error.onProgressstaysanywith reason — Importer/Exporter share this options bag, export side passes ExportProgressState here.services/plugins/PluginService.ts— 3 removed, 2 left.loadManifestToObject(path): Promise<any>→Promise<Record<string, unknown>>;plugin dispatchaction typed viaPlugin.PluginDispatchCallback;readDirStatsfilter/map untyped. Thestore_andplatformImplementation_fields stayanywith reason — test fixtures across app-cli/app-mobile pass partial shapes ({ joplin: {} },{ dispatch, getState }) that the strict interfaces don't accept.services/plugins/Plugin.ts— 2 removed, 0 left. Introduced exportedPluginDispatchCallback;dispatch_/constructordispatchparameter typed;eventEmitter_: any→InstanceType<typeof EventEmitter>.
Checkpoint 6 (2026-05-14): 965 → 949 (16 removed).
services/rest/utils/collectionToPaginatedResults.ts— 3 removed, 1 left.itemscallbacks and sort comparator typed; the outeritems: any[]stays with a new reason — callers pass entity types (NoteEntity, FolderEntity) without index signatures.services/plugins/api/JoplinImaging.ts— 4 removed, 0 left. Introduced localNativeImageLikeinterface (toPNG/toJPEG/resize/crop/getSize);cacheImageandImage.datatyped;toJpgResource/toPngResource.resourceProps→Partial<ResourceEntity>.testing/syncTargetUtils.ts— 4 removed, 0 left. Introduced localTestDataNode/TestDatatypes for the recursive test data structure.time.ts— 5 removed, 1 left.formatLocalToMs/anythingToDateTime/anythingToMs/goBackInTime/goForwardInTimetyped usingstring | number | Date | { toDate }unions; added type-onlyMomentTypesimport forunitOfTimenamespace.msleepswitched fromPromise((resolve: Function))toPromise<void>(resolve => ...)(removes the ban-types disable too).
Checkpoint 7 (2026-05-14): 949 → 929 (20 removed).
services/plugins/api/JoplinWorkspace.ts— 4 removed, 2 left.store→PluginStore;onNoteChangewrapper event typed;selectedNote(): Promise<any>→Promise<NoteEntity | null>. Two left (one is "No plugin-api-accessible Note type defined" reason already; one is theFunctionban-types inonNoteSelectionChange).services/PostMessageService.ts— 4 removed, 1 left.MessageResponse.response/.error→unknown/Error | null;Message.contentandsendResponse.responseContent→unknown.ViewMessageHandlerleftanywith updated reason — callers register handlers with concrete payload types (MessageResponse, SerializableData); making this generic would force changes at every dispatch site.locale.ts— 5 removed, 0 left.supportedLocales_typedRecord<string, Record<string, string[]>>;localeStats_typedRecord<string, Record<string, unknown>>(per-locale stats include pluralForms function);_/_n/stringByLocalerest args →unknown[]. Single inner castas ParsePluralFormFunctionfor the plural-forms field.services/plugins/reducer.ts— 3 removed, 2 left.(view as any)casts onPLUGIN_VIEW_PROP_SET/_PUSH→as unknown as Record<string, unknown[]>etc. The reducer'saction: anykeeps a reason (heterogeneous PLUGIN_* action shapes).viewsByTypereturnsany[]with reason (menu views have menuItems not on PluginViewState).JoplinDatabase.ts— 4 removed, 2 left.TableField.default: any→string | number | boolean | null;tableDescriptions_: any→Record<string, Record<string, string>>;open.options: any→Record<string, unknown>;tableFields.options: any→{ includeDescription?: boolean };createDefaultRow.row: any→Record<string, unknown>.constructor(driver: any)kept with reason — base Database.driver isanyacross multiple driver implementations (sqlite/better-sqlite3/web).
Checkpoint 8 (2026-05-14): 929 → 886 (43 removed).
models/utils/paginatedFeed.ts— 2 removed, 1 left.db: any→JoplinDatabase;WhereQuery.params→(string|number|boolean)[]. Theitems: any[]stays with new reason (callers receive Note/Folder/Resource entities without index signatures).models/settings/settingValidations.ts— 2 removed, 1 left.validateSetting.oldValue/newValue,newValuestypedunknown/Record<string, unknown>. TheValidationHandlertype alias keepsanywith a new reason — settings are heterogeneous; each validator narrows from this base.services/DecryptionWorker.ts— 5 removed, 0 left. Introduced localDecryptionWorkerStartOptionsinterface;dispatchReport.report: any→Record<string, unknown>;dispatch: Function→(action: { type; ... });on/offcallbacks updated reasons (heterogeneous payloads by event name).models/Folder.test.ts— 3 removed, 0 left.foldersById: any→Record<string, FolderEntity & { note_count?: number }>.services/search/SearchEngineUtils.test.ts— 3 removed, 0 left.searchEngine: any→SearchEngine;options: any→NotesForQueryOptions.services/search/SearchEngine.test.ts— 4 removed, 0 left. Introduced localExpectedTermsinterface; helperextractValuenarrowsstring | { value: string }.services/share/ShareService.test.ts— 5 removed, 0 left.extraExecHandlerscallbacks typed; per-handler body casts to specific shapes;Functionban-types disable goes away with the explicit-any ones.services/ExternalEditWatcher/utils.ts— 4 removed, 0 left.spawnCommand.options: any→SpawnOptionsfromchild_process;wrapError.error: any→Error | null;subProcess.on('error')callback typedError; introduced localExternalBridge { openItem }interface.services/interop/InteropService_Importer_Raw.ts— 4 removed, 0 left.itemIdMap/createdResources: any→Record<string, string>/Record<string, ResourceEntity>;folderExists.stats: any[]→Stat[];defaultFolder_: any→FolderEntity | null.services/plugins/ViewController.ts— 2 removed, 2 left.emitMessagereturnsPromise<unknown>,postMessage.message: any→unknown. The other two disables (Store state heterogeneous; storeView shape varies) keep updated reasons.services/spellChecker/SpellCheckerService.ts— 2 removed, 2 left. Removed(a: any, b: any) => ...sort callbacks (already-typed array items work). Two stay with updated reasons (Electron MenuItemConstructorOptions union not imported in lib).services/WhenClause.ts— 6 removed, 0 left.AdvancedExpression.subExpressions: any→Record<string, string>;evaluate/validate.context: any→object(matches existing callers that passWhenClauseContext);createContext.getValueuses generic<T>to matchIContext.getValue<T>.services/plugins/api/JoplinData.ts— 6 removed, 0 left.api_: any→Api;serializeApiBody.body: any→unknown; route calls typed withRequestMethodenum andRequestFile[].
Checkpoint 9 (2026-05-14): 886 → 829 (57 removed).
models/settings/types.ts— 3 removed, 3 left.value: anykeeps a new reason (settings heterogeneous, each consumer narrows);options/showleftanywith reasons (varying per setting/heterogeneous setting access).unitLabel/filterleft where parameters are contravariant per-setting.services/plugins/utils/executeSandboxCall.ts— 4 removed, 2 left.EventHandler.args: any[]→unknown[]; nestedargs: any[]→unknown[]. Recursive walkerargand dotted-pathparent/fnleftanywith new reasons (heterogeneous sandbox object shape).services/interop/InteropService_Exporter_Custom.ts— 5 removed, 1 left. EachCustomImportermethod typed:context: ExportContext,item: BaseItemEntity,resource: ResourceEntity.services/debug/populateDatabase.ts— 6 removed, 0 left.randomIndex/randomElement/randomElementsmade generic<T>;db: any→JoplinDatabase & { clearForTesting };folder/note: any→FolderEntity/NoteEntity.database-driver-better-sqlite.ts— 6 removed, 0 left. Introduced localBetterSqliteDatabase/PreparedStatement/SqliteError/WrappedError/SqlParamstypes.fs-driver-base.ts— 5 removed, 3 left. Introduced exportedFileHandle = unknown(drivers use different concrete shapes) andTarOptions { strict?, portable?, file, cwd }.appendFile/open/close/readFileChunk*typed.ReadDirStatsOptions.recursivemade optional.readFilestaysanywith reason — widening tostring|Buffercascades broken call sites across many lib files.fs-driver-node.ts— 9 removed, 0 left. IntroducedFsError/WrappedFsError;fsErrorToJsError_,setTimestamp,readDirStats,open,close,readFileChunk,tarExtract,tarCreatetyped.services/AlarmServiceDriverNode.ts— 7 removed, 0 left. IntroducedStoredNotification(extends Notification with timeoutId);notifications_/service_/setServicetyped;displayDefault/electronnotification options typed with proper interface;notifier.notifycallback typed.services/plugins/WebviewController.ts— 6 removed, 3 left. Introduced localLayoutItemforfindItemByKey;CloseResponse.resolve/rejecttyped;messageListener_/onMessageuseMessageListenerCallback;store→PluginStore;postMessage/setStorePropvalue typedunknown.registry.ts— 9 removed, 2 left. Various private fields typed (scheduleSyncId_/recurrentSyncId_/db_/showErrorMessageBoxHandler_);setShowErrorMessageBoxHandler/setDb/saveContextHandlertyped;promiseResolvetyped.syncTargets_/scheduleSync.syncOptionskeepanywith reasons (heterogeneous sync target API and Synchronizer.start options).- Side fix:
app-cli/app/command-apidoc.ts— casttableFieldstoMarkdownTableRow[](now thatTableFieldno longer has an index signature compatible with MarkdownTableRow).
Checkpoint 10 (2026-05-14): 829 → 816 (13 removed).
services/UndoRedoService.ts— 8 removed, 2 left.UndoQueue.inner_and methods →unknown;state/redoState/undoState/push.state/schedulePush.state→unknown;dispatch: Functionremoved (no usage). Twoon/offcallbacks stayanywith reason (EventEmitter payloads vary). Follow-up inapp-mobile/.../Note.tsxcasts the undo state to its concrete shape.JoplinServerApi.ts— 5 removed, 3 left.connectionErrorMessage.error→Error | null;requestToCurl_.optionstyped inline;exec/exec_.query/headerstypedRecord<string, unknown>/Record<string, string>;responseJson_→Record<string, unknown>. Three stay with reasons —bodyandfetchOptions.bodyflow throughshim.fetch/fetchBlob/uploadBlob(FetchOptions) which type body asstring;hidePasswordsaccepts both stringified bodies and header records;responseis the shim/blob return.services/ResourceFetcher.ts— 9 removed, 4 left.dispatch: Functiontyped via the action shape;queue_/autoAddResourcesCalls_typed; timer IDs typed viaReturnType<typeof shim.setTimeout>;on/offreasons updated; theas anycast onnotifyDisabledSyncItemscallback replaced with a typed adapter. Four stay with reasons (fetchingItems_mixed bool/Entity;fileApi_/setFileApi/constructorwidened for test mocks).
Checkpoint 11 (2026-05-14): 816 → 805 (11 removed).
services/RevisionService.ts— 7 removed, 0 left.changedSinceCollectionCache_/maintenanceCalls_/maintenanceTimer1_/maintenanceTimer2_typed;noteMetadata_.md→Record<string, unknown>;output.type_cast usesNoteEntity & { type_? }.BaseSyncTarget.ts— 8 removed, 2 left.dispatch: Functiontyped via action shape;initState_/options_typed;option/unsupportedPlatforms/checkConfigtyped. Two stay (db_/fileApi_/constructor.db/setFileApi.v/initFileApi) with reasons — sync target subclasses each pass concrete shapes (FileApi subclasses, FileApiOptions) so tightening here forces every subclass to match.
Checkpoint 12 (2026-05-14): 805 → 789 (16 removed).
models/Resource.ts— 9 removed, 0 left.fsDriver_: any→FsDriverBase;fetchStatusesreturn → typed shape;markupTag.resourcetypedResourceEntity & { alt? };localState/setLocalStateQueries/setLocalState.resourceOrId→ResourceEntity | string;itemCanBeEncryptedcast usesParameters<...>;params: any[]→(string|number)[];resourceConflictFolderreturn type inferred.models/Folder.ts— 7 removed, 0 left.fieldsToLabels: any→Record<string, string>;tableNameToClasses: Record<string, any>→Record<string, typeof BaseItem>(2 places);handleTitleNaturalSorting.optionstyped;allAsTree.options→FolderLoadOptions & { includeNotes? };idToFolders.any→FolderEntityWithChildren;save.options: any→SaveOptions & {duplicateCheck?, reservedTitleCheck?, stripLeftSlashes?}.
Checkpoint 13 (2026-05-14): 789 → 777 (12 removed).
WebDavApi.ts— 4 removed, 5 left.RequestInfo.optionstyped viaFetchOptions & ...;_requestToCurl.optionstyped;serializeRequestcallback body usesRecord<string, unknown>. Five remain with updated reasons (xml2js heterogeneous output, url-parse Url shape, JsonValue alias, fetchOptions/response are platform-specific shapes via shim).file-api.ts— 8 removed, 5 left.RemoteItem.isDiradded (used by list filter);requestCanBeRepeated.errortyped;tryAndRepeatmade generic<T>withFunction→()=> T|Promise<T>;listfilter callbacks typed;put.content: any→string | Buffer | null;multiPut.options: any→{ source? }; introducedBasicDeltaContextand typed the helper;getDirStatFn: Function→(path) => ItemStat[]|Promise<ItemStat[]>; sort/map callbacks typed. Five stay with reasons:RemoteItem.jopItem,PaginatedList.context,driver_, constructordriver, and theoutput: any[]mixed array of ItemStat + deleted-items.
Checkpoint 14 (2026-05-14): 777 → 756 (21 removed).
services/ResourceEditWatcher/index.ts— 10 removed, 3 left.logger_/dispatchtyped;eventEmitter_typedInstanceType<typeof EventEmitter>with proper ES import;externalApi.openAndWatch/watch/stopWatching/isWatchedcallbacks typed{ resourceId: string }; watcher 'all'/'raw' event handlers typed. Three remain with reasons (chokidar typings vary across platforms; on/off heterogeneous events).file-api-driver-joplinServer.ts— 11 removed, 0 left. TypedmetadataToStat_/metadataToStats_arguments; introducedRemoteItemreturn type (addedid: ''to satisfy the interface); typeddelta/list/get/put/multiPutoptions viaExecOptionsfrom JoplinServerApi; typedisRejectedBySyncTargetError/isReadyOnlyError.error; typedObject.entries<...>response shape. Also caught a typo:response.has_more→response.hasMore(the returned object actually exposeshasMore).
Checkpoint 15 (2026-05-14): 756 → 732 (24 removed).
ClipperServer.ts— 11 removed, 2 left. Introduced localClipperDispatchalias for the dispatch field;server_typedhttp.Server & { destroy? }(server-destroy adds.destroyat runtime); all request/response handler callbacks typed viaIncomingMessage/ServerResponse;writeCorsHeaders/writeResponseJson/writeResponseText/writeResponseInstance/writeResponse/execRequesttyped;multiparty.Form.parsecallback typed;request.on('data')typedBuffer | string. Two stay with reasons (actionApishape varies, the Apiroutemethod parameter cast).testing/test-utils.ts— 13 removed, 2 left. TypedswitchClient/setupDatabase/setupDatabaseAndSynchronizeroptions bag; introducedFolderTreeNodeforcreateFolderTree;objectsEqual→Record<string, unknown>;checkThrowAsync/expectThrow/expectNotThrow/checkThrow→ function signatures;id/ids/sortedIds→{ id?: string };at<T>made generic;createNTestNotes.folder→FolderEntity;middlewareCalls_: any[]→boolean[];start.argv→string[]. Two stay (synchronizerStart.extraOptionsandgeneralMiddleware— Synchronizer.start signature and BaseApplication.generalMiddleware override forceany).
Checkpoint 16 (2026-05-14): 732 → 700 (32 removed).
file-api-driver-memory.ts— 6 removed, 0 left. Introduced localMemoryIteminterface foritems_/deletedItems_;encodeContent_/decodeContent_typedstring | Buffer/string;setTimestampreturnPromise<void>;get/put/multiPut/deltaoptions typed viaGetOptions/PutOptions/DeltaOptions;multiPut.outputtyped.eventManager.ts— 4 removed, 0 left.AppStateChangeCallbackmade generic<T>;FilterHandlermade generic<T>(matches plugin api/typesFilterHandler<T>);filterEmit/filterOn/filterOff/appStateOn/appStateOffall generic with internalunknown-cast for storage;stateValue_usesRecord<string, unknown>instead ofany.markdownUtils.ts— 1 removed, 0 left.prependBaseUrlreplace callback_match: any→string.utils/focusHandler.ts— 2 removed, 0 left. Adoptedunknownforelementarg with aMaybeFocusablecast insidetoggleFocusso the runtimetypeofcheck narrows the call. Many call sites pass things likeElement/EventTarget/EditorViewthat have onlyfocus, so any typed interface would have rejected them.utils/joplinCloud/index.ts— 2 removed, 0 left. ExtendedPlanFeaturewith missingbasicInfo/proInfo/teamsInfo/joplinServerBusinessInfo*keys (used at call sites but undeclared);getFeatureLabel/getCellInfousekeyof PlanFeatureindexing withtypeof v === 'string'guards.utils/frontMatter.ts— 3 removed, 0 left.toLowerCase→Record<string, unknown>;parseadded localasString/asNumbercoercion helpers sinceyaml.loadreturnsunknown-shaped values;'tags' in mdcheck now also requiresArray.isArraybefore assigning.utils/ipc/RemoteMessenger.ts— 9 removed, 0 left. Replaced ambientFinalizationRegistryconstructor/registeranywith(id: string)=> voidandobject; Proxygetreturnsunknown;canRemoteAccessProperty.parentObject/trackCallbackFinalization.callback/methodFromPathparent/current/stack typedunknownwith narrowingRecord<string, unknown>casts where indexing is required.file-api-driver-local.ts— 1 removed, 0 left.fsErrorToJsError_.outputtypedError & { code?: JoplinError['code'] }instead ofany.SyncTargetOneDrive.ts— 1 removed, 0 left. Constructoroptions: any→Record<string, unknown>(matches BaseSyncTarget);db: anykeeps reason via// eslint-disable-next-linereferencing BaseSyncTarget.file-api.ts— 1 removed, 0 left. IntroducedListOptionsinterface ({ includeHidden?, includeDirs?, syncItemsOnly?, context? });FileApi.list.options: any→ListOptions.database-driver.ts— 1 removed, 0 left.SelectResult = any→unknownwith a comment explaining narrowing happens at the model layer.database.ts— 1 removed, 8 left with updated reasons. Tried wideningRow/selectAllFields/open/insertQuery/updateQuery/formatValuetounknownbut each cascaded into many downstream errors acrossJoplinDatabase,models/*,services/KvStore,services/RevisionService, etc. (typically requiring index-signature errors, or explicit narrowing for already-extant column accesses). Reverted toanywith descriptive reasons. The one removed wasenumIdusing(this as unknown as Record<string, number>)for the dynamicTYPE_*lookup.wrapQueries/wrapQuerywere already widened safely ((string | SqlQuery | [string, SqlParams?])[]).services/synchronizer/syncInfoUtils.ts— 9 removed, 1 left.masterKeys/masterKeyMaptypedMasterKeyEntity[]/Record<string, MasterKeyEntity>;fetchSyncInfo.output: any→{ version: number; [k: string]: unknown };toObjectreturn type inferred;setWithTimestamp/keyTimestamp/setKeyTimestampall use(this as unknown as Record<...>)casts. Theload.s: anystays — JSON parse output varies per sync target version and is validated per-field via'x' in schecks below.
Checkpoint 17 (2026-05-14): 700 → 681 (19 removed).
services/CommandService.ts— 5 removed, 7 left.CommandContext.dispatch: Functionkept (acts as a variance escape hatch — desktop runtimes type dispatch more strictly viaDesktopCommandContext).ReduxStoresimplified totype ReduxStore = anywith a reason — tests and platforms pass partials and add subscribe etc.scheduleExecute.args: any→unknown.componentUnregisterCommands.commandskeptComponentCommandSpec<any>with updated reason.CommandRuntime.executeandApi.executekeptanywith updated reasons (per-command/per-route shapes).createContext.dispatch.actiontypedunknown.services/rest/Api.ts— 7 removed, 5 left.Request.params: any[]→string[];Request.action?: any→string.RequestContext.dispatch: Function→ typed dispatch.RouteFunctionreason updated.Api.token_: string | Function→string | (()=> string);knownNounces_: any→Record<string, string>;dispatch_: Function→ typed dispatch. Constructor and privatedispatchtyped.actionApi_staysanywith reason.execServiceActionFromRequest_.externalApi: any→ typed.Request.body/bodyJson_/bodyJson/Api.routekeepanywith route-specific reasons.services/interop/InteropService.ts— 4 removed, 2 left.eventEmitter_: any→EventEmitter(changedrequire→ ESimport { EventEmitter } from 'events');on/off.callback: Function→ typed;context: any→{ resourcePaths; destResourcePaths?; notePaths? }.normalizeItemForExportmade generic<T extends Record<string, unknown>>;override: any→Partial<{ is_shared; share_id }>.itemsToExport: any[]/queueExportItem.itemOrId: anykeptanywith updated reasons (mirror exporter signatures across all subclasses; structural { type, itemOrId }).services/interop/InteropService_Exporter_Md.ts— 5 removed, 1 left.makeDirPath_.item→NoteEntity | FolderEntity;replaceLinkedItemIdsByRelativePaths_.item→NoteEntity;replaceItemIdsByRelativePaths_.paths/fn_createRelativePathtyped;prepareForProcessingItemTypecontext inner type narrowed;processItem.item→NoteEntity | FolderEntity. The outerprepareForProcessingItemType.itemsToExport: any[]stays with reason matching InteropService.
Checkpoint 18 (2026-05-14): 681 → 620 (61 removed).
services/interop/InteropService_Exporter_Md.test.ts— 21 removed, 0 left. The sameitemsToExport: any[]/queueExportItemboilerplate appeared 10 times; replaced all withconst { items: itemsToExport, queue: queueExportItem } = createExportItems();(the helper already existed at the top of the file). One inlinecontext: any→ typed shape. Resource.load call sites castitemOrId as string.services/interop/InteropService.test.ts— 11 removed, 0 left.fieldsEqualmade generic<T extends object>;Item.object: any→unknown; export callback signatures typed viaBaseItemEntity/ResourceEntity;Folder.allresult typed;result: anytyped inline;format: 'testing' as any→as ExportModuleOutputFormat.services/synchronizer/synchronizer_MigrationHandler.test.ts— 10 removed, 0 left.MigrationTests.[key]: Function→()=> Promise<void>; the 10items.filter((i: any) => ...)callbacks rely on RemoteItem inference now.services/synchronizer/ItemUploader.test.ts— 6 removed, 0 left. IntroducedMultiPutItem/MultiPutResult/ItemBodyCallbackaliases;ApiCall.args: any[]→unknown[];clearArraymade generic;newFakeApi/newFakeApiCalltyped; theargs[0].lengthaccesses now cast viaMultiPutItem[].services/rest/routes/notes.ts— 13 removed, 0 left. IntroducedStylesheet,ImageSize, exportedImageSizes.htmlToMdParser_→HtmlToMd;RequestNote.id: any→string,anchor_names: any[]→string[],stylesheets: any→Stylesheet[],image_sizes→ImageSizes.requestNoteToNote.output: any→NoteEntity.tryToGuessExtFromMimeType.response: any→ headers shape.replaceUrlsByResources.imageSizestyped; the replace callback_match: any→string.attachImageFromDataUrl.note: any→NoteEntity,cropRect: anytyped.extractNoteFromHTML.imageSizestyped.
Checkpoint 19 (2026-05-14): 620 → 574 (46 removed).
models/settings/builtInMetadata.ts— 28 removed, 0 left. The 20show: (settings: any)callbacks all inherit the type fromSettingItem.show?(settings: any); dropping the explicit annotation removes the disable comments while keeping the interface as-is.themeOptions/ multipleoptions()callbacks: any→Record<string|number, string>;filter: (value: any)→ infers from interface;'notes.sharedSortOrder'value cast toRecord<string, unknown>. Side fix:services/sortOrder/PerFolderSortOrderService.tswidens the read-back to{ field?: string; reverse?: boolean } & Record<...>.Synchronizer.ts— 18 removed, 4 left. Introduced localProgressReportinterface (errors,state?,startTime?,completedTime?, counter index signature) typed acrossprogressReport_/reportHasErrors/completionTime/reportToLines/logSyncSummary.isCannotSyncError.error: anytyped;downloadQueue_: any→TaskQueue(constructor now passes logger via TaskQueue's second arg instead of mutating a private field);setEncryptionService.v: any→EncryptionService;logSyncOperation.localtyped{ id?, path?, type_? }. Several lambda callbacks typed:startAutoLockRefresh.error: any→Error;result.items.filter((it: any))infers from itemsThatNeedSync.local = resource as any→ cast throughtypeof local.(action: any) => dispatch(action)typings dropped (infer). Four stay with reasons:apiCalldispatch by name (FileApi heterogeneous methods),start.options: any(caller bag),handleCannotSyncItem.item: any(BaseItem.saveSyncDisabled), and the BaseItem.saveoptionsbag during DELTA. Side fix:cancel()now usesvoid this.downloadQueue_.stop()(was already an awaitable promise but unmarked).
Checkpoint 20 (2026-05-14): 574 → 539 (35 removed).
models/Setting.ts— 17 removed, 7 left.appType: 'SET_ME' as any→as AppType;saveTimeoutId_/changeEventTimeoutId_: any→ReturnType<typeof shim.setTimeout>;rows.map((r: any))typed inline;toPlainObject.keyToValues: any→Record<string, unknown>;incValue.inc: any→number;setArrayValue.settingValue: any[]→string[];objectValue/setObjectValue.value: any→unknown;enumOptionLabel/isAllowedEnumOption.value: any→unknown/string;subValues.output: any→Record<string, unknown>;groupMetadatasBySections.{generalSection,nameToSections}: any→SettingMetadataSection/Record<string, SettingMetadataSection>. The 7 that stay (with reasons):SettingValueTypefallback,CacheItem.value,keychainService_and its setter (concrete class in app-desktop/app-mobile),valueToString/formatValue(heterogeneous values per setting type), and theconstants_lookup invalue(). Side fix:markupLanguageUtils.tsnow coerces with!!forpluginOptions.enabled.models/Note.ts— 11 removed, 13 left.PreviewsOptions.conditionsParams: any[]→(string|number|boolean)[];geolocationCache_/dueDateObjects_: any→ typed shapes.linkedItemIds.linkstyped{ itemId: string }[].replaceResource{Internal,External}ToInternalLinks.options: any→{ useAbsolutePaths? }.sortNotes.orders: any[]→{ by: string; dir: string }[];noteFieldCompmade generic; sort prop access typedstring|number|boolean.previewFields.options: any→{ includeTimestamps? }.loadFolderNoteByField.value: any→string|number|boolean.preview.options: any→{ fields?: string|string[]; excludeConflicts? }. IntroducedDuplicateOptionsforduplicateMultipleNotes/duplicate. The 13 that stay are mostly around the dynamic(newNote as any)[field]patterns retained asRecord<string, unknown>casts andsearch.optionsforwarding to BaseItem.search. Side fixes:models/Note.test.tstestCases typed[boolean, string, string][].
Checkpoint 21 (2026-05-14): 539 → 506 (33 removed).
shim.ts— 11 removed, 25 left.msleep_resolver typedPromise<void>(no Function).isElectronwindow/process casts use structural types instead ofany.fetchRequestCanBeRetried.error: any→{ code?, message? }.fetchText.options→FetchOptions.fetchBlob/uploadBlob/imageFromDataUrlkeepanywith descriptive reasons. Throwing stub methods now have concrete return types where unambiguous (stringByteLength: number,appVersion: string,pathRelativeToCwd: string,writeImageToFile.format: string,setReact/setReactDomtyped viatypeof React). Several stayanyfor genuine cross-platform polymorphism:Geolocation,electronBridge_,fsDriver_,httpAgent_,proxyAgent,nodeSqlite_,sjclModule, the node datagram module,requireDynamic,fetchWithRetry.fetchFn,setTimeout/setIntervalreturn types,openUrl(boolean vs Promise),readLocalFileBase64(string vs Promise).BaseApplication.ts— 10 removed, 9 left.eventEmitter_: any→EventEmitter(changedrequire→ ESimport);scheduleAutoAddResourcesIID_typed viaReturnType<typeof shim.setTimeout>;currentFolder_: any→FolderEntity.on(callback: Function)→ typed;switchCurrentFolder.folder→FolderEntity;refreshNotes.state: any→State(withparentType: string|numberto allow reassign to ModelType numeric);resourceFetcher_downloadComplete.eventtyped;reducerActionToString.actiontyped{ type; [k]: unknown };applySettingsSideEffects.action/sideEffectstyped;readFlagsFromFile.flags: anyreplaced withflagArgs: string[]. The 9 that stay are around the redux middleware (generalMiddleware/generalMiddlewareFn/reducer/initRedux/dispatch/start) where per-app state and action unions diverge, plus the refreshFolders dispatch wrapper.onedrive-api.ts— 12 removed, 7 left. IntroducedOneDriveAuthinterface andListenerCallbacktype.auth_/setAuth/auth()typed;accountProperties_→Record<string, unknown>;listeners_: Record<string, ListenerCallback[]>;dispatch.param/setAccountPropertiestyped;oneDriveErrorResponseToError.errorResponsetyped (return stillanysince downstream augments withrequest/headers/bodyfields);execTokenRequest.body/refreshAccessToken.body→Record<string, string>;authorizationTokenRemoved.datatypedunknownwithRecord<string, unknown>inner cast. The 7 remaining (uploadChunk,uploadBigFile,exec,execJson,execText,handleRequestRepeat.error,oneDriveErrorResponseToErrorreturn) cover FetchOptions/handle/buffer bags and network error augmentation.
Checkpoint 22 (2026-05-14): 506 → 477 (29 removed).
services/e2ee/EncryptionService.ts— 12 removed, 13 left.EncryptionCustomHandler<Context = any>→<Context = unknown>.EncryptOptions.onProgress: Function→(event: { doneSize: number })=> void.activeMasterKeyIdandloadedMasterKeyerror throws now useError & { code; masterKeyId? }casts instead ofany.wrapSjclError.sjclErrortyped.randomHexString/generateMasterKeyContent_castshim.randomBytestonumber[]at use site.stringWriter_typed inline;fileReader_/fileWriter_encoding params →string;encryptString/decryptStringplain/cipher text →string.headerTemplateindexes viaRecord<number, { fields: (string|number)[][] }>;encodeHeader_parameter typed;decodeHeaderString.cipherText→string;decodeHeaderBytes_.output→Record<string, string|number>.itemIsEncrypted.itemtyped{ encryption_applied?, encryption_cipher_text?, type_? }. The 13 that stay (with reasons) arefsDriver_,encryptAbstract_/decryptAbstract_/decodeHeaderSource_source/destination (duck-typed reader/writer union), and a handful of polymorphic API surface bits.services/plugins/api/types.ts— 8 removed, 16 left.Command.executereason updated.ExportModule.onProcessItem/onProcessResourcereasons updated.KeymapItem.userData/ImportContext.options/Script.onStarttypedunknown/Record<string, unknown>.MenuItem.commandArgs: any[]→unknown[].FormValue.value/DialogResult.formData→unknown/Record<string, unknown>.SettingItem.options→Record<string|number, string>.ContentScriptModuleLoadedEvent.userData→unknown. The 16 staying are all plugin API surface tied to external libraries (markdown-it, CodeMirror 6) where the concrete types aren't imported in lib. Side fix:app-mobile/components/plugins/dialogs/PluginDialogWebView.tsxcastsformDatafromSerializableDatatoRecord<string, unknown>at the call site.
Checkpoint 23 (2026-05-14): 477 → 433 (44 removed).
reducer.ts— 33 removed, 14 left. IntroducedAdditionalReducer(kept as a 3-anyinterface — reducers from plugin/share services are immer-based with Draft mutation and per-sub-state shapes) andSearchEntryforState.searches.StateLastSelectedNotesIds.{Folder,Tag,Search}: any→Record<string, string[]>;StateDecryptionWorker.decryptedItemCounts→Record<string, number>;State.masterKeys: any[]→MasterKeyEntity[];pluginsLegacy/syncReport/screenstyped.derivedStateCache_→Record<string, unknown>;cacheEnabledOutputmade generic<T>. Selectors andselectArrayShallowtyped.StateUtils.{selectArrayShallow,notesOrder,foldersOrder,lastSelectedNoteIds}typed;arrayHasEncryptedItems→{ encryption_applied? }[];removeAdjacentDuplicatesmade generic.(windowDraft as any)[k]patterns replaced with(windowDraft as unknown as Record<string, unknown>)[k]. HelpersupdateOneItem,handleHistory,getContextFromHistory,removeItemFromArray, the top-levelreducerbody keepanywith descriptive reasons matching the redux-action-union discrimination pattern. Side fixes:PLUGINLEGACY_DIALOG_SETandDECRYPTION_WORKER_SETreducer cases use typed intermediates.models/BaseItem.ts— 11 removed, 20 left.ItemsThatNeedDecryptionResult.items: any[]→BaseItemEntity[];isSystemPathandpathToIduse localconst/splitchains instead of mutating ananyvariable;loadItemByField.value: any→string|number|boolean;syncItemClassNames/syncItemTypesmap callbacks typed;encrypterror andreducedItemcasts now go through structural types;decrypt,serialize,unserialize,serialize_format/unserialize_formatkeptanywith descriptive reasons for their unavoidably heterogeneous parameters.updateSyncTimeQueries/saveSyncTimekeptanybecause tests pass loose objects withidas number.
Checkpoint 24 (2026-05-14): 433 → 411 (22 removed).
BaseModel.ts— 22 removed, 19 left.typeEnum_: any[]→[string, ModelType][].saveMutexes_→Record<string, { acquire }>.setDb.db: any→JoplinDatabase.defaultValues.output: any→Record<string, unknown>;applySqlOptions.params/all.paramstyped as primitive arrays;allIds.rows.maptyped inline.count.options/loadByField.fieldValue/loadByFields.fields/loadByTitle.fieldValue/fieldType.defaultValue/releaseSaveMutex.release/saveMutex.modelOrIdall typed concretely.saveQuery.temp/filtered: any→Record<string, unknown>.userSideValidation.otyped{ id?, user_updated_time?, user_created_time? }.count.thencallback typed. Several stayanywith descriptive reasons:addModelMd,byId,modelIndexById(subclass overrides return per-entity types so a base-class generic conflicts with subclass return-type variance),removeUnknownFields/new/save/modOptions/saveQuery.o/saveQuery.query/diffObjects/modelsAreSame/modelSelectAll<T = any>(heterogeneous entity slices across subclasses), anddispatch: Functionfor variance.
Checkpoint 25 (2026-05-14): 411 → 361 (50 removed).
components/shared/note-screen-shared.ts— 21 removed, 2 left. IntroducedAttachedResource/AttachedResources/SaveNoteOptions/AttachFileAsset/ResourceHandlertypes.BaseState.noteResources: any→AttachedResources.Sharedinterface methods all typed (saveOneProperty/noteComponentchange/installResourceHandling/uninstallResourceHandling/attachedResources/toggleCheckboxRange).saveNoteButton_press.options: any→SaveNoteOptions.newState: any→Partial<BaseState>. `resourceCache: any→AttachedResources.toggleCheckboxLinereturn →ToggleCheckboxResultdiscriminated union, downstream callers handle the string|tuple shape.setState(state: any)kept with reason (React component setState signature).ResourceHandlerparameterany[]kept with reason (EventEmitter heterogeneous payloads). Side fixes:app-desktop/.../useWebviewIpcMessage.tshandles the new string return;Note.tsxresource handler signature is already compatible via variadicany[]`.shim-init-node.ts— 12 removed, 9 left. IntroducedProxySettingsinterface;proxySettings.anyandsetupProxySettings.options.anytyped.ShimInitOptionsanyfields kept with descriptive reasons (sharp/keytar/React/electronBridge/nodeSqlite are external module types).detectAndSetLocale.Setting: any→typeof Setting.saveOptions: any→ typed structurally.imageOptions: any→ typed.cleanUpOnError/file/request.on('error')typedError.requestOptionskeptany(node http/https request options + agent).sitescall site typed viaNodeJS.CallSite[].makeResponse.responsetyped structurally.import-enex.ts— 17 removed, 3 left.sourceStream/destStream.on('error')typedError.removeUndefinedPropertiesmade generic<T>.saveNoteResources.toSave: anycast tightened viaPartial<Pick<...>>for the delete keys.Node.attributes: Record<string, any>→Record<string, string>.saveNoteToStoragecast throughas ExtractedNote.handleSaxStreamEventtyped(...args: any[])=> voidwith reason (sax events heterogeneous).noteAttributes/noteResourceAttributestypedRecord<string, string>.createErrorWithNoteTitletyped. Stream and saxStreamon('error')typedError.noteResource[n]indexing uses a typed cast throughRecord<string, string>.is_todocast to0|1. Latitude/longitude/altitude needas unknown as numbercasts since ENEX values arrive as strings.
Checkpoint 26 (2026-05-14): 361 → 323 (38 removed).
services/search/SearchEngine.ts— 22 removed, 0 left.ComplexTerm.scriptType: any→string.dispatch: Function→ action-shape dispatch.syncCalls_: any[]→boolean[].scheduleSyncTablesIID_typed viaReturnType<typeof shim.setTimeout>.setDb.db: any→JoplinDatabase.fieldNamesFromOffsets_.offsets: any[]→number[].hitsThisRow/docsWithHitstypedUint32Array.processBasicSearchResults_/processResults_typedProcessResultsRow[]+ParsedQuery.queryTermToRegex.term: any→string.basicSearch.searchOptions: anytyped structurally;determineSearchType_.preferredSearchType→SearchType;allTerms: any[]→Term[]. Side: addedfuzziness?: numbertoProcessResultsRow(was being assigned at runtime but not declared).import-enex-md-gen.ts— 16 removed, 5 left.Section.lines: any[]keptanywith reason (mixed strings/Section/Hr objects).ParserState.{anchorAttributes,spanAttributes}: any[]→Record<string, string>[].collapseWhiteSpaceAndAppend.state: any→ParserState. IntroducedSaxContexttype alias;cssValue/isInvisibleBlock/isHighlight/isCodeBlock/displaySaxWarningtyped viaSaxContextand{ style?: string }.attributeToLowerCase.nodetyped structurally.isSpanWithStyle/isSpanStyleBold/isSpanStyleItalic.attributestyped.saxStream.on('error')typedError;saxStream.on('opentag')node: any→{ name; attributes? }.captionLines: any[]lets inference do the work.enexXmlToMdArray.stream,renderLine/renderLines.lines, andcurrentCellskeepanywith descriptive reasons (sax stream / heterogeneous nested Section objects).
Checkpoint 27 (2026-05-14): 323 → 293 (30 removed).
Scatter cleanup across many smaller files:
services/synchronizer/LockHandler.ts— 2 removed, 0 left.RefreshTimer.id: any→ReturnType<typeof shim.setInterval>.lockFileToObject.file: anytyped structurally with optional path/updated_time.services/synchronizer/utils/types.ts— 1 removed, 1 left with updated reason.LogSyncOperationFunction.local: anytyped structurally;ApiCallFunctionkeepsany[]/any(dispatches by name across drivers).services/style/themeToCss.ts— 2 removed, 0 left.isColor.vtypedunknownwith type predicate; theme indexing viaRecord<string, unknown>cast.services/style/cssToTheme.ts— 2 removed, 0 left.declarations/outputtyped; final return castas unknown as Theme.services/profileConfig/mergeGlobalAndLocalSettings.ts— 2 removed, 0 left.rootSettings/subProfileSettings/outputallRecord<string, unknown>.utils/attachedResources.ts— 2 removed, 0 left.resourceCache_/output: any→AttachedResources.services/noteList/renderTemplate.ts— 2 removed, 0 left.Cell.value: any→unknown;valueToString.value: any→unknown(usingString(value)).services/plugins/api/JoplinWindow.ts— 2 removed, 0 left. Introduced localDispatchStoretype for store_.services/plugins/api/Global.ts— 2 removed, 0 left.implementation: any→BasePlatformImplementation;store: any→Store<any>with descriptive reason;process: any→NodeJS.Process.services/plugins/api/Joplin.ts— 1 removed, 1 left with updated reason.store: any→Store<any>with reason.require.return: anykept with plugin-API reason.services/plugins/api/JoplinCommands.ts— 2 removed (reasons updated).services/plugins/utils/loadContentScripts.ts— 1 removed, 1 left.loadedModule as anycast → typed extension.ExtraContentScript.module: anykept with descriptive reason.services/ResourceService.ts— 2 removed, 0 left.maintenanceTimer1_/maintenanceTimer2_: any→ReturnType<typeof shim.setTimeout/setInterval>.services/KeymapService.ts— 2 removed, 0 left.modifiersRegExp: any→RegExp;domToElectronAccelerator.event: anytyped structurally with the 5 fields actually read.services/interop/InteropService_Importer_Base.ts— 2 removed, 0 left.setMetadata.md: any→Partial<ImportMetadata>with cast;init.options: any→ImportOptions.services/interop/InteropService_Importer_Custom.ts— 2 removed, 0 left.options: any→Record<string, unknown>onCustomImporter.onExec.context.optionsandprocessedOptions.services/interop/InteropService_Exporter_Raw.ts— 2 removed, 0 left.processItem.item/processResource.resourcetyped viaBaseItemEntity/ResourceEntity.utils/ipc/utils/mergeCallbacksAndSerializable.test.ts— 1 removed, 1 left.data: anytyped inline.utils/ipc/RemoteMessenger.test.ts— 2 removed, 0 left.transfer.o: anymade generic<T>;testObjects: any[]→Record<string, unknown>[].- Side fix:
services/interop/InteropService_Importer_Md.test.tsusesImportModuleOutputFormatenum members rather than raw string literals.
Checkpoint 28 (2026-05-14): 293 → 272 (21 removed).
Scatter cleanup chasing remaining "Old code before rule was applied" comments across many files:
models/Note.ts— 5 removed, 7 left.previewFieldsWithDefaultValues.optionstyped;(n as any)[field]and(o as any)[field]patterns →Record<string, unknown>casts;beforeChangeItems: any→Record<string, string | null>;updateNoteOrder_.order→number;handleTitleNaturalSorting.optionstyped.models/BaseItem.ts— 4 removed, 16 left.displayTitle.itemtyped structurally;items.map((item: any))typed viamodelSelectAll<{ id: string }>;markdownTag.itemOrId/isMarkdownTag.mdtyped structurally;save.oreason updated.BaseModel.ts— 3 removed, 16 left.isNew.object/optionstyped;filterArray/filterreasons updated; static enum loop usesRecord<string, ModelType>cast.models/Folder.ts— 1 removed, 0 left.report: any→Record<string, number>.services/plugins/Plugin.ts— 1 removed, 0 left.emit.event: any→unknown.services/plugins/api/JoplinPlugins.ts— 1 removed, 0 left.script.onStart.catch.error: anytyped structurally.services/commands/MenuUtils.ts— 1 removed, 0 left.commandToStatefulMenuItem.commandTarget: any→unknown.services/interop/InteropService_Exporter_Md_frontmatter.ts— 0 removed (reason updated).import-enex.ts— 1 removed, 2 left. saxStream cdata callback typedstring.testing/test-utils-synchronizer.ts— 0 removed (kept with reason matching the heterogeneous test fixtures).services/CommandService.test.ts— 1 removed, 1 left.createCommand.optionskeptanywith reason (test fixtures), execute mock kept with reason.services/synchronizer/Synchronizer.conflicts.test.ts— 2 removed, 0 left. dynamic field iteration usesRecord<string, unknown>casts.services/rest/Api.test.ts— 2 removed, 0 left.response: any→NoteEntity; sort callback typed{ id: string }.utils/ipc/utils/mergeCallbacksAndSerializable.test.ts— 1 left with reason (mergeCallbacksAndSerializable return shape).
packages/lib summary
Final: 1138 → 272 (866 removed, 76% reduction), processed in 28 batches over 2026-05-13 → 2026-05-14.
Of the 272 remaining disables, 270 have descriptive -- reason comments. The 2 without are inside commented-out code in services/synchronizer/synchronizer_LockHandler.test.ts:103-105.
The remaining any annotations cluster into a handful of structural reasons that resist simple narrowing:
- Per-app polymorphism:
shim.tsandshim-init-node.tscross-platform shims (sharp, keytar, react, electron bridge, nodeSqlite, fsDriver, httpAgent, proxyAgent, node datagram module), redux store/state/dispatch differing across cli/desktop/mobile, FileApi driver subclass shapes. - Plugin API surface:
services/plugins/api/types.ts,Joplin.ts,JoplinCommands.ts, command/script/content-script entry points where args/returns are arbitrary by design — narrowing would break plugin authors. - External library types not imported here: markdown-it, CodeMirror 6 (EditorView/Extension/CompletionSource), sax stream callbacks, node http/https request bag, css-tools declarations, sharp instance.
- BaseModel/BaseItem subclass variance:
byId,modelIndexById,save,serialize,filter, etc. are overridden by every subclass with stricter per-entity types; narrowing the base forces subclass return-type incompatibilities or many call-site casts. - Reducer action unions:
reducer.tsmatches dynamically onaction.typeacross all redux actions in the app; the action union diverges across cli/desktop/mobile so a strict union here would not compose. - Test fixtures: a handful of tests pass loose
{ id: 1, type_: ... }objects withidasnumber, mocked stores withoutdispatch, or partial entity slices that don't satisfy the production-typed signatures.
All yarn tsc --noEmit and yarn linter-ci packages/lib/ runs pass for every batch commit.