Skip to main content

any Cleanup Progress - details

packages/pdf-viewer

Session date: 2026-05-11

Files processed:

  • messageService.ts — 1 removed, 0 left. Replaced data?: any with data?: Record<string, unknown>.
  • Page.tsx — 1 removed, 0 left. Replaced let style: any with let style: CSSProperties (imported from react).
  • PdfDocument.ts — 3 removed, 0 left. Imported PDFDocumentProxy and PDFPageProxy from pdfjs-dist; typed doc, pages (as Record<number, PDFPageProxy>) and the local pdfDocument.

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 any with {} 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. Imported Argv and ArgumentsCamelCase from yargs; typed builder callbacks as (yargs: Argv) => ... and handler args as ArgumentsCamelCase<{ 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: isPosition type guard now uses Partial<DocumentPosition> instead of any; removeOverlay overlay param uses OverlayType<unknown> instead of OverlayType<any>.
    • Left: OptionUpdateCallback newVal/oldVal: any (the source is value: any, which is API-driven; narrowing in callbacks would be a logic change); addOverlay return type (the any structurally deceives the base-class signature SearchQuery | undefined to allow returning { clear: () => void } from the decorator branch — fixing it is a class-hierarchy refactor, not a typing tweak); commands as any cast (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.
  • CodeMirror/pluginApi/PluginLoader.ts — 4 removed, 1 left.
    • Removed: introduced PluginLoaderWindow type 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: OnScriptLoadCallback exports: any (already tagged "Plugin exports have dynamic structure").

Files skipped entirely (only non-"Old code" tags inside):

  • types.tsexecCommand/varying argument types.
  • CodeMirror/editorCommands/editorCommands.tsEditorCommandFunction varying 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. findParentElementByClassName parameter typed Element | null (broad enough to accept EventTarget & Element from callers in app-desktop).
  • splitCommandString.ts — 1 removed, 0 left. Introduced local SplitCommandStringOptions { handleEscape?: boolean } interface.
  • cli.ts — 0 removed, 1 left. Tried Interface from readline/promises, but @types/node in this repo does not declare the readline/promises submodule. Updated reason on the disable comment to reflect that.
  • execCommand.ts — 1 removed, 1 left. env typed as Record<string, string | undefined>. The other entry is already tagged with a Workaround reason (Expo/NodeJs.ProcessEnv conflict).
  • net.ts — 1 removed, 0 left. Introduced local FetchWithRetryOptions extends RequestInit interface with retry, callback, pause fields.
  • object.ts — 0 removed, 2 left. objectValueFromPath does successive indexing (result = result[e]) which requires any; tightened to Record<string, unknown> failed because intermediate values are unknown. checkObjectHasProperties is called with NoteEntity / 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 own attributesHtml already typed the same way.
  • Logger.ts — 17 removed, 1 left.
    • Removed: TargetOptions.console typed Console; Logger.create wrapper args unknown[]; addTarget field copy uses paired Record<string, unknown> casts; objectToString outer param unknown, inner Error branch uses a typed intersection cast; objectsToString, error/warn/info/debug rest args unknown[]; items: unknown[]; global-logger fallback typed as a Logger cast; consoleObj[fn] indexed through Record<string, (...args: unknown[]) => void> cast.
    • Left: TargetOptions.database — tightening leaks through lastEntries() to all downstream consumers (e.g. app-mobile/exportDebugReport.ts) that read .timestamp/.level/.message via any. Refactoring those is out of scope. Updated reason on the disable comment.
  • 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 as Record<string, Record<string, unknown>> so it can be spread), MarkupRenderer.render, MarkupRenderer.allAssets, MarkupToHtmlConverter.render (also tightened options: anyRenderOptions), MarkupToHtmlConverter.allAssets.
    • Left: FsDriver.cacheCssToFile — return is used loosely as a RenderResultPluginAsset push target while the actual returned object only has { path, mime }; tightening forces logic changes. Updated reason on the comment.
  • HtmlToHtml.ts — 2 removed, 0 left. Both theme: anyRendererTheme. Also made Options.ResourceModel optional (constructor already handles null).
  • MarkupToHtml.ts — 4 removed, 0 left. rawMarkdownIt_ typed as MarkdownIt (added a type-only import type * as MarkdownItType from 'markdown-it'). The as any on the RendererClass construction went away by branching to explicit new MdToHtml(this.options_) / new HtmlToHtml(this.options_) calls. theme: any on render and allAssetsRendererTheme.
  • MdToHtml.ts — 19 removed, 4 left.
    • Removed: RendererRule.install (typed via PluginContext/RuleOptions/unknown), RendererRule.assets ((theme: RendererTheme) => PluginAsset[]), RendererRule.plugin (MarkdownIt.PluginWithOptions); RendererPlugin.module and options (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 four PluginContext any fields (Record<string, string> for css, Record<string, PluginAsset[]> for pluginAssets, InMemoryCache for cache, Record<string, Record<string, unknown>> for userData); RuleOptions.theme (RendererTheme); pluginOptions_ (NonNullable<Options['pluginOptions']>); loadExtraRendererRule.module (matches new ExtraRendererRule['module']); allProcessedAssets.theme, allProcessedAssets.assets (PluginAssets); allAssets.theme and inner assets (PluginAssets); render.theme (RendererTheme); _attrs: any on highlight()string.
    • Left: outputAssetsToExternalAssets_(output: any) — the function mutates with delete output.cssStrings, which RenderResult does not permit; updated reason on the comment. render's theme: any = null was tightened to theme: RendererTheme = null (note: keeping null default required RendererTheme to allow null via callers passing it through, handled because all fields are optional). highlight() returns any because the function returns either a string or { wrapCode, html } depending on whether rules.fence is 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 calling markdownIt.use(plugin, options); tightening forces logic restructuring. Updated reasons.
  • noteStyle.ts — 1 removed, 0 left. theme: anyRendererTheme (all fields optional, so theme = theme ? theme : {} continues to type-check).
  • headerAnchor.ts — 2 removed, 0 left. Added type-only imports for MarkdownIt and markdown-it/lib/rules_core/state_core; markdownIt: anyMarkdownIt, state: anyStateCore.
  • InMemoryCache.ts — 0 removed, 3 left. Generic cache: values are heterogeneous across calls (different keys store different shapes); unknown would 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 for MarkdownIt and linkify-it; markdownIt: anyMarkdownIt, self: anyLinkifyIt.LinkifyIt.
  • MdToHtml/linkReplacement.ts — 1 removed, 0 left. LinkReplacementResult.resource: anyResourceEntity | null.
  • MdToHtml/renderMedia.ts — 1 removed, 0 left. Options.theme: anyRendererTheme.

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: anyMarkdownIt, state: anyStateCore / StateInline / StateBlock, tokens: any[]Token[], Token: anytypeof 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: anyRuleOptions (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. Typed defaultRender: Renderer.RenderRule; outer markdownIt: anyMarkdownIt; rule signature gets proper types.
  • MdToHtml/rules/externalEmbed.ts — 0 removed, 2 left. Both (self.renderToken as any)(...) casts pass env, self which renderToken's typed signature does not declare. Updated reasons.
  • MdToHtml/rules/checkbox.ts — 5 removed, 0 left. theme: anyRendererTheme; Token: anytypeof import('markdown-it/lib/token'); sourceToken: anyToken; markdownIt: anyMarkdownIt; state: anyStateCore.
  • MdToHtml/rules/fence.ts — 2 removed, 1 left. Typed the rule signature with Token[] / MarkdownIt.Options / Renderer. Left: options: any on the fence renderer — options.highlight here returns either a string or { wrapCode, html }; the typed signature can't express that disjunction without restructuring MdToHtml.render. tmpToken as Token cast added because the rule constructs a partial Token ({ attrs }) just to call slf.renderAttrs, which only reads attrs.
  • MdToHtml/rules/image.ts — 2 removed, 0 left. markdownIt: anyMarkdownIt; rule signature gets typed.
  • MdToHtml/rules/fountain.ts — 4 removed, 0 left. theme: anyRendererTheme (the existing comment said "Theme is defined in @joplin/lib and we don't import it here" — but now we have a local RendererTheme); markdownIt: anyMarkdownIt; rule signatures typed.
  • MdToHtml/rules/highlight_keywords.ts — 4 removed, 0 left. Token: anytypeof import('markdown-it/lib/token'); markdownIt: anyMarkdownIt; state: anyStateCore.
  • MdToHtml/rules/katex.ts — 9 removed, 0 left. Introduced KatexMacroToken, KatexMacro, KatexOptions local interfaces (matching the macro shape documented in the file's own comments). stringifyKatexOptions, renderToStringWithCache, and the inner toSerialize now use these types. (t: any) => t.text typed via the KatexMacroToken.text field. state: anyStateInline / StateBlock; markdownIt: anyMarkdownIt; the two renderer functions typed via Token[]. A single as KatexOptions['macros'] cast on the macros pulled from options.context.userData.__katex.macros, since userData is typed Record<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 as Renderer.RenderRule; handleImageTags now returns Renderer.RenderRule; outer markdownIt: anyMarkdownIt; inner content.replace callback uses string instead of any.
  • MdToHtml/rules/link_close.ts — 3 removed, 0 left. defaultRender: Renderer.RenderRule; markdownIt: anyMarkdownIt; rule signature typed.
  • MdToHtml/rules/mermaid.ts — 3 removed, 1 left. theme: anyRendererTheme; markdownIt: anyMarkdownIt; rule signature typed. Left: the (self.renderToken as any)(tokens, idx, options, env, self) cast in the fence-rule fallback — same pattern as abc.ts / externalEmbed.ts, passes extra args beyond the typed renderToken signature.
  • MdToHtml/rules/sanitize_html.ts — 3 removed, 0 left. markdownIt: anyMarkdownIt; state: anyStateCore; tokens: any[]Token[].
  • MdToHtml/rules/link_open.ts — 2 removed, 0 left. markdownIt: anyMarkdownIt; rule signature typed.
  • MdToHtml/rules/source_map.ts — 2 removed, 0 left. params: anyRuleOptions (added mapsToLine?: boolean to RuleOptions); 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 always true).
  • generate-images.ts — 1 removed, 0 left. output: any[](string | number | undefined)[] (matches the heterogeneous values pushed from Operation).
  • generate-database-types.ts — 5 removed, 0 left. Imported Table and Column from @rmp135/sql-ts/dist/Typings; createRuntimeObject/generateListRenderDependencyType/the two .map() callbacks all typed via those. The column-push uses as Column because adding the missing ColumnDefinition fields explicitly may change sql-ts.fromObject() output.
  • update-readme-contributors.ts — 1 removed, 0 left. The request callback typed (error: Error | null, response: { statusCode: number }, data: Contributor[]).
  • release-clipper.ts — 1 removed, 0 left. Introduced local Manifest = 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 as main(process.argv)).
  • setupNewRelease.ts — 1 removed, 0 left. yargs .argv cast to { _: string[]; updateVersion?: string; updateDependenciesVersion?: string } (rather than yargs.Arguments<T>, because Arguments._ is Array<string | number> and parse-numbers: false guarantees strings at runtime — using the library type would require a _[0] as string cast at every call site).
  • tool-utils.ts — 11 removed, 0 left. saveGitHubUsernameCache(cache: any)Record<string, string>; execCommand options typed { cwd?; env?; maxBuffer? } and its callback (error: (Error & {signal?: string}) | null, stdout: string, stderr: string); execCommandWithPipes error: Error, code: number | null; setPackagePrivateField(value: any)boolean; downloadFile's https.get callback response: import('http').IncomingMessage, error: Error; fileSha256 stream data: Buffer and error: Error; fileExists stat callback error: NodeJS.ErrnoException | null; gitHubLatestRelease/gitHubLatestRelease_KeepInCaseMicrosoftBreaksTheApiAgain response: any deleted (node-fetch's inferred Response works fine); githubRelease(options: any){ isDraft?: boolean; isPreRelease?: boolean }.
  • build-release-stats.ts — 0 removed, 1 left. createMarkdownTable is typed (headers, rows: MarkdownTableRow[]) where MarkdownTableRow = Record<string, string>. Release rows contain numeric counts (windows_count, etc.), so the cast rows as any[] masks a real type mismatch. Tightening would require widening createMarkdownTable in @joplin/lib (cross-package change). Updated reason.
  • licenses/licenseChecker.ts — 1 removed, 0 left. enforceString(line: any)string | string[] | undefined | null (matches the Array.isArray(line) ? line.join(', ') : ... branches).
  • website/updateNews.ts — 1 removed, 0 left. Introduced local RssFeedItem interface (rss is untyped); feedItems: RssFeedItem[].
  • website/processDocs.ts — 3 removed, 0 left. currentLinkAttrs?: any[string, string][] | null; imported Token from markdown-it/lib/token so processToken(token: Token, …); onopentag attrs: Record<string, any>Record<string, string> (matches htmlentities(attrs[n]) usage and the local attributesHtml signature).
  • 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. onopentag attrs: anyRecord<string, string>.
  • website/utils/convertLinksToLocale.test.ts — 1 removed, 0 left. [string, any, string][][string, Partial<Locale>, string][] with a locale as Locale cast at the call site (the test cases only supply pathPrefix, not the full Locale shape).
  • website/utils/frontMatter.ts — 3 removed, 0 left. yaml.load(...) cast directly to FrontMatter; formatFrontMatterValue(value: any)FrontMatter[keyof FrontMatter]; the (header as any)[key] = … assignment refactored to a separate Record<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: anyStateCore (imported from markdown-it/lib/rules_core/state_core).
  • utils/translation.ts — 1 removed, 0 left. Introduced local GettextTranslation and GettextParsed interfaces capturing only the fields read (gettext-parser is require-d and has no types). serializeTranslation's parameter in build-translation.ts was incorrectly typed as string — corrected to Parameters<typeof parseTranslations>[0].
  • utils/discourse.ts — 5 removed, 0 left. Introduced DiscourseApiError extends Error { apiObject; status }; new Error(...) as DiscourseApiError then setting error.apiObject/error.status directly. response.json() as any deleted — node-fetch's inferred return works fine for the downstream .error.status === 404 reads. createTopic/createPost/updatePost body params → Record<string, string | number> (matches execApi).

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. extractPluginFilesFromPackage typed existingManifests: PluginManifests, return Promise<PluginManifest>; files.find((f: any) => …)(f: string) (since fs.readdir returns string[]); commitMessage parameters typed (manifest: PluginManifest | null, previousManifest: PluginManifest | null, error: Error | null); readManifests/writeManifests return/take PluginManifests; the four let X: any = {} locals in processNpmPackage typed; the yargs handlers refactored — setSelectedCommand and the three command handlers now share a CommandArgs type matching the { pluginRepoDir, dryRun } shape both commandBuild and commandUpdateRelease expect (the previous selectedCommandArgs = '' declaration also implicitly drifted from string → object); commands: Record<string, Function> replaced with Record<string, (args: CommandArgs)=> Promise<void>>. The two await readJsonFile(...) callers in this file pass an explicit type parameter (readJsonFile<PluginManifest>(...), readJsonFile<{ version: string }>(...)) since readJsonFile is 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 local PluginVersionStats and PluginStats types and used them for createStats/saveStats.
  • lib/checkIfPluginCanBeAdded.ts — 2 removed, 0 left. caseInsensitiveFindManifest/default-export both typed via PluginManifests and PluginManifest.
  • lib/overrideUtils.ts — 3 removed, 0 left. applyManifestOverrides(manifests: PluginManifests, …); the inner manifest[propName] = propValue write uses as PluginManifest & Record<string, unknown> because we mutate via a for...of over Object.entries(override) keys. getObsoleteManifests made generic over <T extends { _obsolete?: boolean }> so the existing test (which passes manifest-shaped fixtures rather than ManifestOverride-shaped ones) types correctly without rewriting its data.
  • lib/errorsHaveChanged.test.ts — 1 removed, 0 left. testCases: any[][][ImportErrors, ImportErrors, boolean][]; dropped the as any cast in the destructuring loop.
  • lib/validateUntrustedManifest.ts — 1 removed, 0 left. Parameters typed manifest: PluginManifest, existingManifests: Record<string, PluginManifest>.
  • lib/updateReadme.test.ts — 1 removed, 0 left. manifests: anyRecord<string, PluginManifest>.
  • lib/updateReadme.ts — 2 removed, 0 left. Parameter manifests: Record<string, PluginManifest>; the rows.push(manifests[pluginId]) line uses an as unknown as MarkdownTableRow cast (the existing code stores PluginManifest objects inside a MarkdownTableRow[] and createMarkdownTable is typed for Record<string, string> rows — the same impedance mismatch documented on build-release-stats.ts last session); the rows.sort((a: any, b: any) => …) callback's args type-infer from MarkdownTableRow directly.
  • lib/utils.ts — 2 removed, 0 left. readJsonFile made 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's manifestOverrides: anyRecord<string, { _obsolete?: boolean; description?: string; [key: string]: unknown }> (matches the heterogeneous fixture). Second test's manifests: any → inferred from the literal.
  • lib/gitCompareUrl.ts — 1 removed, 0 left. Widened to Pick<PluginManifest, 'repository_url' | '_publish_commit'> (matches the fields the function actually reads, so the test fixtures don't need to spell out the whole PluginManifest).
  • lib/checkIfPluginCanBeAdded.test.ts — needed touching even though it had no disable comments: the test passes partial fixtures, so call sites cast via as unknown as Parameters<typeof checkIfPluginCanBeAdded>[0/1]. Same pattern in lib/validateUntrustedManifest.test.ts (as unknown as PluginManifest) and lib/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)=> void so callers like Note.tsx's (event: EditorChangeEvent)=> void still type-check.
  • utils/getVersionInfoText.ts — 1 removed, 0 left. Replaced (global as any).HermesInternal with (global as { HermesInternal?: unknown }).HermesInternal.
  • components/screens/ConfigScreen/types.ts — 1 removed, 0 left. UpdateSettingValueCallback value param anyunknown (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-storage ships no .d.ts and no @types/... is installed, so introduced local SqliteDb/SqliteResultSet interfaces covering the two methods used (executeSql, returning rows/insertId). The base DatabaseDriver interface uses SelectResult = 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 by ResourceFetcher.eventEmitter_.emit('downloadComplete', ...)).
  • index.web.ts — 1 removed, 0 left. Removed the Root as any cast — Root extends React.Component and AppRegistry.registerComponent's ComponentProvider is () => React.ComponentType<any>, which () => Root satisfies.
  • utils/fs-driver/fs-driver-rn.ts — 7 removed, 2 left (down from 9).
    • Removed: appendFile/writeFile content param anystring (base class uses string); rnfsStatToStd_ param any → local RnfsStatLike union of StatResultT | ReadDirResItemT | DocumentFileDetail with in-checks; readDirStats options anyReadDirStatsOptions (with stats: RnfsStatLike[]); close(handle: any)unknown (handle is ignored); readFileChunk(handle: any) → typed inline { path: string; offset: number; mode: string; stat: { size: number } | null }; tarExtract/tarCreate options anyOmit<Parameters<typeof tar*>[0], 'cwd'> & { cwd?: string }.
    • Left: the inner output: any[] in readDirStats — entries can be either DocumentFileDetail (SAF) or the normalized Stat-shaped object from rnfsStatToStd_, the recursion helpers also accept this heterogeneous shape, so a single concrete type would force restructuring. Reason updated on the disable comment.
  • services/AlarmServiceDriver.android.ts — 2 removed, 0 left. @joplin/react-native-alarm-notification ships no types, so introduced local ScheduledAlarm { id: string; data?: { joplinNotificationId?: number } } covering the two fields read.
  • services/AlarmServiceDriver.ios.ts — 6 removed, 0 left. Imported PushNotification, PushNotificationPermissions, ScheduleLocalNotificationDetails from @react-native-community/push-notification-ios. Notes: requestPermissions options { alert: 1, badge: 1, sound: 1 } → booleans (the typed interface declares them as boolean?; runtime accepts both, but the type is stricter). hasPermissions return tightened to Promise<boolean> and perm.alert && perm.badge && perm.sound wrapped in !!() since the fields are optional booleans. scheduleNotification's iosNotification keeps id: string and a final as ScheduleLocalNotificationDetails cast because the library's typed ScheduleLocalNotificationDetails declares neither id nor alertBody? — but the runtime requires id (and alertBody may be omitted), so the cast preserves existing behavior.
  • root.tsx — 4 removed, 5 left.
    • Removed: generalMiddleware inner scheduleRefreshFolders((action: any) => storeDispatch(action), ...) simplified to scheduleRefreshFolders(storeDispatch, ...); componentDidUpdate(prevProps: any) and UNSAFE_componentWillReceiveProps(newProps: any) typed as AppComponentProps (this also forced adding the missing syncStarted: boolean field that mapStateToProps populates 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.shareURL has a pre-existing typing inconsistency (declared ()=> string but assigned both as a function and a string literal) so typing the event would expose that unrelated bug; one await reduxSharedMiddleware(store, next, action, storeDispatch as any) — kept as any cast since the upstream signature uses Dispatch and storeDispatch is already typed loosely. Reasons updated on the disable comments.

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: anystring (consumers JSON.parse it). Required casting the data field in index.jest.tsx's test mock to as 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-style style={[{...inline...}, props.style]}.
  • components/ExtendedWebView/index.jest.tsx — 1 removed, 0 left. additionalProps: anyRecord<string, unknown>; kept inline comment explaining the HACK.
  • components/screens/ConfigScreen/SettingsToggle.tsx — 1 removed, 0 left. value: anyboolean.
  • components/NoteList.tsx — 1 removed, 1 left. dispatch: (action: any)=> voidDispatch (from redux). Left: styles_: Record<string, StyleSheet.NamedStyles<any>>NamedStyles<T> requires T to be the same record being passed; tightening to unknown breaks property access. Reason updated.
  • components/app-nav.tsx — 1 removed, 2 left. dispatch typed Dispatch; screens widened to Record<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 inner ComponentType<any> for screens. Reasons updated.
  • components/SelectDateTimeDialog.tsx — 3 removed, 0 left. Replaced PureComponent<any, any> with PureComponent<SelectDateTimeDialogProps, SelectDateTimeDialogState> (local interfaces capturing themeId, shown, date, onAccept, onReject and state shape).
  • components/ScreenHeader/index.tsx — 12 removed, 1 left. Added local type ScreenHeaderStyles = ReturnType<typeof StyleSheet.create> and replaced every inner button factory's styles: any parameter with it (12 occurrences). Left: styleObject builder uses Record<string, any> because it incrementally mixes ViewStyle, TextStyle, and IconStyle entries spread from theme.icon — splitting into typed sub-objects would force restructuring. Reason updated.
  • components/screens/ConfigScreen/SettingComponent.tsx — 3 removed, 0 left. value: anyunknown (callers pass arbitrary setting values; the inner branches narrow before forwarding). output: any = nullReact.ReactElement | null = null. items as any (for the Dropdown options from enumOptionsToValueLabels) → items as unknown as DropdownListItem[] because enumOptionsToValueLabels returns { [computedKey]: string }[] where the runtime values happen to be label/value but the type system can't tell. Forwarding to SettingsToggle/ValidatedIntegerInput/SettingTextInput adds 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 local SettingsMap type 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: any builder — same heterogeneous-style-record pattern as ScreenHeader.styleObject. Reason updated.
  • components/screens/ConfigScreen/ConfigScreen.tsx — touched (not yet processed for disable count, but renderToggle's value={value}value={!!value} to satisfy the narrowed SettingsToggle.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/inline onLayout events → LayoutChangeEvent (imported from react-native); renderButton/addSettingButton options → { description?: string; statusComp?: ReactElement; disabled?: boolean }; handleSetting/settingToComponent/inner updateSettingValue value: anyunknown; renderFeatureFlags's output: 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; unknown forces casts at every read. Reason updated.
  • components/screens/Note/Note.tsx — 13 removed, 4 left. emptyArray: any[]never[]; lastSavedNote: anyNoteEntity | null; styles_: anyRecord<string, ReturnType<typeof StyleSheet.create>>; noteTagDialog_closeRequested: any()=> void; refreshResource(resource: any)ResourceEntity; focusUpdateIID_: anyReturnType<typeof setTimeout> | null; folderPickerOptions_: anyFolderPickerOptions (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: any builder (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 — matches state.syncReport: any in @joplin/lib/reducer and Synchronizer.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[]; the args[0]?.name / args[0]?.args reads cast through { name?: string; args?: unknown[] }.
  • services/plugins/PlatformImplementation.ts — 2 removed, 1 left. Components.[key: string]: anyunknown; registerComponent(component: any)unknown. Left: get nativeImage(): any — matches BasePlatformImplementation.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 anyas ((...args: unknown[])=> void) | undefined; outer wrapper signature (...args: any[])unknown[]. Left: the } as any; cast at end of wrapLogFunction — assigning a generic (...args: unknown[])=> void to a specific console method requires going through any because TypeScript treats console[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 from react-native-webview/lib/WebViewTypes).
  • contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.ts — 1 removed, 0 left. postMessageHandler(message: any): Promise<any>unknown / Promise<unknown> (matches ContentScriptData.postMessageHandler: (message: unknown)=> unknown in @joplin/editor/types).
  • contentScripts/rendererBundle/useWebViewSetup.ts — 1 removed, 0 left. pluginOptions: anyPluginOptions (imported from @joplin/renderer/MarkupToHtml); inner enabled: 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 missing unobserve() and disconnect() methods so the class satisfies the ResizeObserver interface 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 as any because 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 3 Old code before rule was applied entries got their reason rewritten; the 5 Assigning types... would be too big of a refactoring entries were already correctly tagged.
  • utils/types.ts — 0 removed, 2 left (reasons updated). AppState.route and AppState.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 (heterogeneous styleObject builder — reason documented in the second batch).
  • components/screens/LogScreen.tsx — 0 removed, 1 left (heterogeneous styles builder — 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: any in side-menu-content.tsx (1); nativeImage: any in PlatformImplementation.ts (1) — total 2. Tightening requires updating the lib base types first.
  • Per-screen ConfigScreen settings: settings: Record<string, any> in ConfigScreen.tsx (4 occurrences: state, props, sectionToComponent, renderFeatureFlags) — total 4. Tightening to unknown forces casts at every settings[key] read across the file.
  • Per-component fields that the library doesn't type: editorRef/dialogbox/menuOptionsCache_/styles builder in Note.tsx (4) — total 4.
  • NamedStyles<any> in NoteList.tsx (1) — TypeScript pattern limitation.
  • console[key] = ... as any in wrapConsoleLog.ts (1) — TypeScript pattern limitation (intersection of console method signatures).
  • output: any[] in fs-driver-rn.ts (1) — readDirStats output mixes SAF DocumentFileDetail and normalized Stat-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. removeElement typed generically as <T>(array: T[], element: T). Left: unique(array: any[]) — tried generic <T>, but BaseModel.loadByIds calls it with string[] | number[] and TS can't unify T across the union, forcing a cast at the call site (a wider blast radius). Reason updated.
  • utils/cache.ts — 4 removed, 0 left. CacheEntry.object: anystring (always JSON-stringified); setAny/setObject o: anyunknown; getAny return Promise<any>Promise<unknown> (existing as object cast at the one public consumer remains valid).
  • utils/errors.ts — 3 removed, 0 left. ErrorOptions.details?: anyunknown; ApiError.details: anyunknown; errorToPlainObject(error: any)unknown with 'httpCode' in error narrowing followed by (error as { httpCode?: number }).httpCode casts on each field read (TS's in operator doesn't narrow unknown to a typed shape, only to object).
  • services/MustacheService.ts — 1 removed, 1 left. The local layoutView: any in renderViewRecord<string, unknown>. Left: View.content?: any — tried tightening to Record<string, unknown>, but routeHandler.ts:61 reads view.content.error.httpCode and other dynamic paths; views contribute heterogeneous content shapes per route. Reason updated.

Files attempted but reverted (still any):

  • commands/BaseCommand.tsrun(argv: any). Tried yargs.Arguments, but subclasses (e.g. CompressOldChangesCommand, StorageCommand) narrow argv to a per-command Argv interface, and TS function-parameter contravariance forbids that without making the whole class generic — which would propagate through every BaseCommand[] consumer. Reason updated.
  • utils/urlUtils.tssetQueryParameters(query: any). Callers pass Koa ParsedUrlQuery (Record<string, string | string[]>), pagination shapes with numbers, and plain string records. Tightening forces fixes at every call site. Reason updated.
  • config.tsinitConfig(overrides: any). Tried Partial<Config>, but Config.resourceDir: string is required and only set by some test overrides; the existing spread relies on any to 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 two as any casts in value<T> use the existing type parameter (as T). The local value: any in readThenWrite becomes await this.value<Value>(key) — the explicit type param uses the public Value = number | string already 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 is Buffer). Inner assignment uses content as Buffer.
  • models/UserItemModel.ts — 1 removed, 0 left. Dropped as any on loadByIds(options.byUserItemIds as any)byUserItemIds is already typed number[] and loadByIds accepts string[] | number[].
  • models/UserDeletionModel.ts — 0 removed, 1 left. end(error: any): tried Error, but tests pass plain strings. Tried Error | string, but errorToString requires Error; wrapping strings in new Error() changes runtime output (adds a stack field to the serialized payload). Reason updated.
  • models/utils/pagination.ts — 4 removed, 0 left. requestPaginationOrder(query: any)ParsedUrlQuery | PaginationQueryParams with as string / as PaginationOrderDir narrowing on the read fields; requestPagination(query: any)(Pagination & PaginationQueryParams) | null; filterPaginationQueryParams(query: any)PaginationQueryParams | null; paginateDbQuery made generic over <T = unknown> for PaginatedResults<T> and the local orderSql: any[] inferred from .map.
  • utils/views/table.ts — 2 removed, 0 left. Table.requestQuery?: anyPaginationQueryParams; makeTablePagination(query: any)ParsedUrlQuery (imported from querystring).
  • utils/views/select.ts — 0 removed, 2 left. Tried Record<string, unknown>, but callers pass concrete entity types like User (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's groupBy('user_id') as anyas unknown as { item_count: number; user_id: Uuid }[] (Knex's typed builder doesn't carry the aggregate column shape through db.raw).
  • utils/testing/koa/FakeRequest.ts — 2 removed, 0 left. Introduced local FakeNodeRequest { method?: string } (the only field used).
  • utils/testing/koa/FakeResponse.ts — 4 removed, 0 left. body: anyunknown; headers_: anyRecord<string, string>; set/get params/return → string.
  • utils/testing/fileApiUtils.ts — 2 removed, 0 left. getDelta return type and inner cast PaginatedResults<any>PaginatedResults<unknown>.
  • models/items/storage/testUtils.ts — 1 removed, 0 left. let error: any = nullError & { 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. All numbers: any[]number[]; numberToDateName(value: any, type: any)(value: number | string, type: 'dow' | 'mon') (the function does value - 1, so wrapping in Number()); dateList(numbers: any[], type: any)(numbers: number[], type: 'dow' | 'mon'); introduced local type LaterSchedule = Record<string, number[]> and used it for removeFromSchedule, scheduleToSentence; removeFromSchedule(schedule, member: any, length: any)(LaterSchedule, string, number); the four cronspec: any/numDates: any handlers typed as string / number; the final (window as any).prettyCron cast tightened to (window as Window & { prettyCron?: Record<string, unknown> }).prettyCron.
  • utils/routeUtils.test.ts — 4 removed, 0 left. Three testCases: any[] typed as tuple arrays ([string, string, string, ItemAddressingType][], [string, {...}][], [string, string[]][]). routes: Record<string, any> typed Record<string, number> with three as unknown as Parameters<typeof findMatchingRoute>[1] casts at call sites (the test injects numbers in place of Router instances).
  • routes/api/sessions.test.ts — 8 removed, 0 left. Eight (context.response.body as any).id casts → as { id: string }.
  • routes/api/items.test.ts — 4 removed, 0 left. tree: anyRecord<string, Record<string, null>>; PaginatedResults<any> (×2) → <unknown>; result.items as anyas unknown as SaveFromRawContentResult.
  • routes/api/shares.test.ts — 3 removed, 0 left. tree: anyRecord<string, Record<string, null>>; both PaginatedResults<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).value on a querySelectorquerySelector<HTMLInputElement>('input[name=email]').value.
  • routes/admin/users.test.ts — 2 removed, 0 left. postUser(props: any) / patchUser(user: any) typed Record<string, unknown> (tests intentionally pass max_item_size: '' which Partial<User> would reject).
  • routes/index/stripe.test.ts — 2 removed, 0 left. WebhookOptions.stripe?: anyReturnType<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 inner as any return 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: anyunknown; SubRequestResponse.header: Record<string, any>Record<string, unknown>.
  • routes/api/batch_items.ts — 2 removed, 0 left. PaginatedResults<any><unknown>; the inner as any cast → as unknown as unknown[].
  • models/UserModel.test.ts — 3 removed, 0 left. The three syncInfo*: any test fixtures share a single inline object-shape type with optional ppk (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 anyas string.
  • routes/index/items.test.ts — 1 removed, 0 left. items: anyRecord<string, Record<string, never>>.
  • models/items/storage/StorageDriverS3.ts — 2 removed, 0 left. Introduced local ReadableLike interface (only the 3 listener overloads used) so stream2buffer is 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: anyStorageDriverConfig & { enabled?: boolean }.
  • routes/api/users.ts — 1 removed, 0 left. bodyFields<any>bodyFields<Partial<User>> (fromApiInput accepts a partial user).
  • routes/api/users.test.ts — 1 removed, 0 left. results: anygetApi<{ items: User[] }>.
  • routes/api/ping.test.ts — 1 removed, 0 left. body as anyas { status: string; message: string }.
  • models/LockModel.test.ts — 2 removed, 0 left. 'wrongtype' as anyas unknown as LockType (and the same for LockClientType).
  • db.migrations.test.ts — 2 removed, 0 left. dbSchemaSnapshot return → Awaited<ReturnType<typeof sqlts.toTypeScript>>; the db as any cast → 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[]) → local ItemTree3Node interface; checkContextError's body: any → cast through as { code?: ErrorCode }; setupAppContext({} as any, ...)as unknown as AppContext. Left: AppContextTestOptions.request: any (httpMocks.RequestOptions is too narrow; callers pass files: { file: { path: string } } and free-form bodies); appContext: any inside koaAppContext (intentionally mocks only a subset of AppContext, cast at return); the createBaseAppContext one was removed. Reasons updated on the two remaining ones.
  • utils/requestUtils.ts — 2 removed, 7 left. Two safe removals: the outer IncomingMessage cast in formParse uses as unknown as FormParseRequest; the bodyFields/bodyFiles req: any typed IncomingMessage. The other entries (BodyFields, FormParseResult.files, FormParseRequest.body, convertFieldsToKeyValue return) were attempted but reverted — Record<string, unknown> breaks Fields/Files compatibility (formidable's File | File[] union surfaces .filepath access errors), and BodyFields widening propagates to every route handler that reads body.fields.email etc. 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: anyunknown; respondWithItemContent(koaResponse: any) → local KoaResponseLike interface with just body and set(). Left: RouteHandler's ...args: any[], Promise<any> — concrete handlers (login, mfa, users) narrow args to per-route field types; tightening propagates through every route. Reason updated. (Plus a downstream cast responseObject.response as typeof ctx.response in routeHandler.ts:56 for the now-unknown response.)
  • 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 anyas Config['tableNameCasing'].
  • models/utils/pagination.test.ts — 2 removed, 0 left. testCases: any[Record<string, unknown> | null, Pagination][]; the inline input: any removed by inferring from the tuple. Inner literal dir: 'asc' switched to PaginationOrderDir.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. Tightened FileViewerResponse.body/ResourceInfo/LinkedItemInfo to concrete shapes (Buffer | string, NoteEntity etc.); unserializeJoplinItem / serializeJoplinItem typed against NoteEntity; getResourceInfos output uses the named ResourceInfos alias; jopItem and itemToRender use NoteEntity & { ...optional fields }; FileToRender.contentBuffer | null (the null as any cast is no longer needed). Left: the renderOptions: any for markupToHtml.render@joplin/renderer's RenderOptions is loosely typed in that package; tightening would require updating renderer first.
  • db.ts — 14 removed, 0 left. ConnectionCheckResult.error/latestMigration: anyError | null and { name: string } | null; the slow-query handler connection: any, bindings: any[]DbConnection and unknown[]; the inner queryInfos: Record<any, QueryInfo> and timeoutId: anyRecord<string, QueryInfo> and ReturnType<typeof setTimeout>; filterBindings(bindings: any[]): Record<string, any>unknown[] / Record<string, unknown>; KnexQueryErrorData.bindings: any[]unknown[]; migrateList's migrations: any typed as the actual [string | { file } | { name | file }][] tuple via a MigrationInfo alias; isNoSuchTableError/isUniqueConstraintError error: any{ code?: string; message?: string } | null | undefined. The pg setTypeParser callback's val: anystring.
  • utils/testing/apiUtils.ts — 11 removed, 0 left. All the body: Record<string, any> parameters (×9 functions) → body: object (entity types like User, FormUser don't have an index signature, so Record<string, unknown> rejects them — object accepts both concrete entities and plain records). The query: 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 of Error & { httpCode?: number; code?: string } / PlainObjectError / null (callers read .httpCode on it; errorToPlainObject is also assigned to it). objectToApiOutput inner as any[k] casts → (output as Record<string, unknown>)[k]. allForDebug typed (Omit<Item, 'content'> & { content?: string | Buffer })[] and now spreads instead of mutating. Left: itemToJoplinItem(): any (heterogeneous Note/Folder/Resource/Tag return); the matching joplinItem?: any field; the inner joplinItem: any local. Reasons updated.
  • models/BaseModel.ts — 7 removed, 0 left. SaveOptions.validationRules/previousItem, DeleteOptions.validationRules, ValidateOptions.rulesRecord<string, unknown>. all() rows: any[] cast → as T[] only. fromApiInput local output: anyRecord<string, unknown> with as Record<string, unknown> on the input spread and as T on the return. isNew's 'id' in (object as any)typeof object === 'object' && object && 'id' in object narrowing.
  • app.ts — 7 removed, 0 left. defaultEnvVariables: Record<Env, any>Record<Env, Partial<EnvVariables>>. markPasswords(o: Record<string, any>)object parameter (concrete entity types like DatabaseConfig lack index signatures) with an internal cast to Record<string, unknown> for iteration. getEnvFilePath(argv: any){ envFile?: string }. argv: Argv = yargsArgv as anyas unknown as Argv. The commandArgv._ cast → as Argv & { _: string[] }. main().catch((error: any))(error: Error).
  • utils/testing/testRouters.ts — 3 removed, 2 left. exec callback error/stdout/stderr: any(Error & { signal?: string }) | null / string / string. serverProcess: anyReturnType<typeof spawn>. checkAndPrintResult(result: any)unknown with in narrowing. Left: curl return type (it parses heterogeneous JSON responses that callers read without narrowing) and the response: any in main() (same reason). Reasons updated.
  • utils/testing/shareApiUtils.ts — 5 removed, 0 left. Introduced local LegacyTreeNode and ShareTreeNode interfaces (with [key: string]: unknown on ShareTreeNode so test fixtures can include arbitrary fields like title). convertTree(tree: any) / createItemTree3(tree: any[]) / shareFolderWithUser(itemTree: any) use those types.
  • routes/admin/users.ts — 5 removed, 0 left. boolOrDefaultToValue/intOrDefaultToValue/makeUser fields: anyRecord<string, unknown>. The field reads in makeUser cast via as string / as number per User shape. error: any on admin/users/:id route handler → Error | null. accountTypeOptions().map((o: any))(o: { value: number; selected?: boolean }). The formParse().fields is cast as 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; StripeRouteHandler return → 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 single outputAsRecord = output as unknown as Record<string, unknown> alias.
  • routes/index/users.ts — 3 removed, 0 left. makeUser(fields: any)Record<string, unknown> with as string narrowing on field reads; error: any on users/:id GET → 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.itemToJoplinItem plus the two joplinItem: any locals 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.ts yesNoOptions/yesNoDefaultOptions; app.ts config initConfig overrides.
  • Loose lib-typed defaults (1): config.ts initConfig(overrides: any)Partial<Config> requires resourceDir.
  • Heterogeneous test fixtures / API call results (3): testRouters.curl and response in main (parsed JSON responses); array.ts unique (TS can't unify T across string[] | number[]).
  • TypeScript pattern limitations (6): testUtils.AppContextTestOptions.request; the 4 in requestUtils.ts (BodyFields, FormParseResult.files, FormParseRequest.body, convertFieldsToKeyValue — all centered on formidable's Fields | Files union not allowing narrowing).

packages/app-desktop

Session date: 2026-05-13Branch: any_refactor_6

Files processed:

  • commands/exportFolders.ts — 1 removed, 0 left. _context: anyCommandContext.
  • commands/exportNotes.ts — 1 removed, 0 left. _context: anyCommandContext.
  • commands/focusElement.ts — 1 removed, 0 left. _context: anyCommandContext.
  • commands/toggleExternalEditing.ts — 1 removed, 0 left. mapStateToTitle(state: any)AppState.
  • checkForUpdates.ts — 0 removed, 1 left. parentWindow: any is passed to bridge().showMessageBox(parentWindow, ...) but the bridge signature is showMessageBox(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?: anyReact.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 the typeof === 'string' narrowing already in the body); needs as Error after 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: anyWindowCommandDependencies.
  • 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 local ShowRevisionsDependencies (NoteEditor passes a different shape with setShowRevisions/isInFocusedDocument for this command only — not registered via useWindowCommandHandler).
  • gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinCommands.ts — 0 removed, 1 left. CodeMirror: any — same reason as useExternalPlugins.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 by emitContentScriptMessage which already accepts unknown).
  • 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?: anystring (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 (matches fs.writeFile's accepted data types).
  • gui/WindowCommandsAndDialogs/commands/addProfile.ts — 1 removed, 0 left. comp: anyWindowControl; onClose(answer: string)unknown with as string (matches PromptOptions.onClose shape).
  • 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) needs as string for previously-loose item.context.pluginId.
  • gui/ResizableLayout/utils/persist.test.ts — 1 removed, 0 left. layout: anyLayoutItem (the type was already imported).
  • gui/ResizableLayout/utils/style.ts — 0 removed, 1 left. styled-components .attrs typing 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?: anyRecord<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: any is spread into theme.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: anyRefObject<NoteBodyEditorRef>. Tightening exposed two execCommand() floating-Promise calls; fixed with void (same void pattern as showLocalSearch).
  • gui/NoteEditor/utils/useMessageHandler.ts — 1 removed, 0 left. editorRef: anyRefObject<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: anyRef<unknown> (must use Ref, not RefObject, to accept ForwardedRef callers).
  • gui/NoteEditor/utils/useSearchMarkers.ts — 1 removed, 0 left. keywords: any[]{ value: string; type?: string; accuracy?: string }[] (matches both Keyword-consuming code and the literal from useNoteSearchBar).
  • gui/NoteList/utils/useOnNoteClick.ts — 1 removed, 0 left. (event.target as any)HTMLElement.
  • gui/NoteList/utils/useScroll.ts — 1 removed, 0 left. event: anyReact.UIEvent<HTMLDivElement>; switched event.target to event.currentTarget (target is EventTarget without 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 anyas InsertAt (now exported from useDragAndDrop).
  • gui/NoteListHeader/utils/validateColumns.test.ts — 1 removed, 0 left. (props: any)Partial<NoteListColumn>[].
  • gui/WindowCommandsAndDialogs/commands/showNoteContentProperties.ts — 1 removed, 0 left. comp: anyWindowControl.

Checkpoint 5 (2026-05-13):

  • gui/WindowCommandsAndDialogs/commands/showNoteProperties.ts — 1 removed, 0 left. comp: anyWindowControl.
  • 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)spellCheckerConfigMenuItems returns a heterogeneous menu shape that doesn't satisfy Electron's MenuItemConstructorOptions structurally. 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)unknown with as PromptSuggestion<T> cast inside.
  • gui/dialogs.ts — 1 removed, 0 left. options: any = nullRecord<string, unknown>.
  • gui/hooks/usePrevious.ts — 1 removed, 0 left. Dangling disable comment (no any in 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: anyunknown with internal Record<string, unknown> narrowing after JSON.parse(JSON.stringify(o)).
  • integration-tests/util/setMessageBoxResponse.ts — 0 removed, 1 left. Mock return cast prevents breakage when Electron's MessageBoxReturnValue shape evolves. Reason updated; redundant block comment removed.
  • tools/notarizeMacApp.ts — 0 removed, 1 left. appBundleId is no longer in @electron/notarize's NotaryToolStartOptions but still required at runtime. Reason updated.
  • utils/checkForUpdatesUtilsTestData.ts — 3 removed, 0 left. releases1/2/3: anyunknown as GitHubRelease[] cast at the end of each array literal (fixtures include extra GitHub API fields not in GitHubRelease).
  • 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 any cast wasn't needed; ReactPortal already assignable to ReactNode.
  • 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: any matches the lib reducer shape; the inner syncCompletedWithoutError already 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: anyReact.Ref<HTMLButtonElement>.
  • gui/ExtensionBadge.tsx — 2 removed, 0 left. style?: anyReact.CSSProperties; themeSelector(_state: any, props: any) → typed by Props.
  • gui/ImportScreen.tsx — 2 removed, 0 left. Local ProgressState interface; 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 on revisionList_onChange removed; webview_ipcMessage(event: any){ channel?: string; args?: unknown[] }.
  • gui/ResizableLayout/utils/setLayoutItemProps.ts — 2 removed, 0 left. props: anyPartial<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: anyCSSProperties / Record<string, CSSProperties>.
  • gui/WindowCommandsAndDialogs/commands/gotoAnything.ts — 2 removed, 0 left. Local PluginMenuItem interface for PluginManager.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/dependencyNames typed as EffectCallback/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: anyRecord<string, unknown>; dependencies: any[]unknown[].
  • gui/lib/SearchInput/SearchInput.tsx — 2 removed, 0 left. inputRef?: anyReact.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 → typed HTMLLinkElement | HTMLScriptElement | null with branch-specific locals.
  • services/plugins/PlatformImplementation.ts — 2 removed, 0 left. Components index → unknown; registerComponent(_, component: any)unknown.
  • services/plugins/UserWebview.tsx — 2 removed, 0 left. theme?: anyRecord<string, unknown>; ref: anyReact.Ref<UserWebviewRef> (new exported interface).
  • gui/NoteEditor/NoteBody/CodeMirror/utils/useEditorSearchHandler.ts — 2 removed, 0 left. searchMarkers: anySearchMarkers; webviewRef: RefObject<any>RefObject<NoteViewerControl>.
  • gui/NoteEditor/NoteBody/CodeMirror/utils/useWebviewIpcMessage.ts — 2 removed, 0 left. Local WebviewIpcEvent { channel?; args? } used for both onMessage parameter and the returned callback; arg0 cast as number at the percentScroll call.
  • 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: any stays (TinyMCE Editor type narrower than getDoc/getWin usage). Reason updated.
  • gui/NoteEditor/utils/useFormNote.ts — 2 removed, 0 left. editorRef: anyRefObject<NoteBodyEditorRef> (exposed a floating Promise in handleAutoFocus; fixed with void); onResourceChange(event: any){ id: string }.
  • gui/NoteEditor/utils/useWindowCommandHandler.ts — 2 removed, 0 left. noteSearchBarRef: anyMutableRefObject<HTMLInputElement | null>; execute(..., ...args: any[])unknown[].
  • gui/NoteList/utils/types.ts — 2 removed, 1 left. themeId: anynumber; resizableLayoutEventEmitter: anyEventEmitter; searches: any[] left with descriptive reason (matches lib reducer shape).
  • gui/NoteListWrapper/NoteListWrapper.tsx — 1 removed, 0 left. resizableLayoutEventEmitter: anyEventEmitter; 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: anyWindowControl; onClose(answer: any, buttonType: string)(unknown, unknown) with as number for todo_due; mapStateToTitle(state: any)State (from lib reducer).
  • gui/NoteListHeader/useDragAndDrop.ts — 3 removed, 0 left. setupDataTransfer.data: anyunknown; onResizerDragOver as any (×2) → as unknown as EventListener for add/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_move made 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)unknown default; record-cast accessors; cast title/tooltip/iconName as string and onClick as (()=> void) | undefined at call sites.
  • InteropServiceHelper.ts — 1 removed, 2 left. Promise<any>Promise<Buffer | null> (this exposed a downstream writeFile(string) mismatch in usePrintToCallback.ts — added as unknown as string there 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's ChangeEventHandler is typed as (event: {value: string})=> void but the runtime hands a DOM React.ChangeEvent through (StyledInput passes the raw onChange). Updated reason on all three handlers.
  • gui/EncryptionConfigScreen/EncryptionConfigScreen.tsx — 2 removed, 1 left. themeId: anynumber; infoComp: any = nullReact.ReactNode. onPasswordInputChange left for the same PasswordInput reason above; reason updated.
  • gui/NoteListControls/NoteListControls.tsx — 3 removed, 0 left. StyleProps = any → local interface; StyledRoot: any typed via interface; iconMap: anyRecord<string, string>.
  • gui/PdfViewer.tsx — 3 removed, 0 left. StyleProps = any → local theme interface; resource: anyResourceEntity; onMessage_(event: any)MessageEvent<{ name; text? }>.
  • gui/ConfigScreen/controls/SettingComponent.tsx — 2 removed, 1 left. onChange(event: any){ value: unknown }; inputStyle: anyCSSProperties. The settingKeyToControl declaration's React.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 optional percent.
  • gui/ResizableLayout/utils/persist.ts — 4 removed, 0 left. saveLayout(layout): anyPartial<LayoutItem>; produce(layout, (draft: any))LayoutItem; delete (item as any)[k]as unknown as Record<string, unknown>; loadLayout(layout: any)Partial<LayoutItem> | null with as LayoutItem on the spread.
  • gui/NoteEditor/utils/resourceHandling.ts — 4 removed, 0 left. commandAttachFileToBody.options → local CommandAttachFileToBodyOptions; resourcesStatus.resourceInfos: anyResourceInfos (renderer); clipboardImageToResource.image: anyNativeImage (electron type import); getResourcesFromPasteEvent.event: any{ preventDefault } | null.
  • gui/SearchBar/SearchBar.tsx — 4 removed, 0 left. inputRef?: anyMutableRefObject<HTMLInputElement | null>; onChange(event: any){ value: string }; onKeyDown(event: any)React.KeyboardEvent; inner document.activeElement as anyas HTMLElement.
  • gui/WindowCommandsAndDialogs/commands/showPrompt.ts — 4 removed, 0 left. comp: anyWindowControl; value?: anystring (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) → single editorAsRecord alias typed Record<string, (...args: unknown[])=> unknown>.
  • bridge.ts — 3 removed, 1 left. showOpenDialog (this.lastSelectedPaths_ as any)[fileType] (×2) → typed keyof LastSelectedPath; shouldShowMenu(_event: any, params: any)(unknown, { isEditable: boolean }). The remaining as any on dialog.showOpenDialog(this.activeWindow(), options as any) stays — our OpenDialogOptions.properties is string[] 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: anySerializedPluginSettings; 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: anyKeyword; match: any{from: {line; ch}; to: {line; ch}} (CodeMirror 5 DocumentPosition); marks: anyReturnType<typeof highlightSearch>[]. The stream: any overlay token signature stays — CM5 StringStream isn't typed in this repo.
  • gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useCursorUtils.ts — 0 removed, 5 left. All CodeMirror: any / cm: any / params: any reasons updated to mention the CM5 dynamic loader / no @types/codemirror.
  • gui/ResourceScreen.tsx — 5 removed, 0 left. onResourceClick/Delete/ToggleSorting's return anyvoid; rootStyle: anyCSSProperties & { height?; width? }; mapStateToProps(state: any)AppState.
  • gui/WindowCommandsAndDialogs/utils/appDialogs.tsx — 0 removed, 5 left. Reasons consolidated — each dialog requires a different customProps shape and the render functions intentionally spread an open shape.
  • app.reducer.ts — 5 removed, 1 left. watchedResources: anyRecord<string, unknown>; navHistory: any[]AppStateRoute[]; createAppDefaultState.resourceEditWatcherDefaultState: anyPartial<AppState> (this required spreading backgroundWindows: {} to override the lib's looser default); getNextLayout.currentLayout: anystring | string[]; (item as any)[propName]as unknown as Record<string, unknown>. The main reducer action: any stays 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>. Needed type="info" on <StyledRecipient> and <StyledShareState> to satisfy the StyledMessage's type: string required prop that the original <any> cast had hidden (runtime branches on type === 'error', so 'info' = default styling).
  • gui/NoteEditor/NoteEditor.tsx — 6 removed, 0 left. onFieldChange.value: anystring; onTitleChange(event: any)React.ChangeEvent<HTMLInputElement>; onBodyWillChange(event: any){ changeId: number }; externalEditWatcher_noteChange(event: any) / onNotePropertyChange(event: any){ id; note: NoteEntity } / { note: NoteEntity } (matches the AlarmChangeEvent shape lib emits); (newFormNote as any)[key]as unknown as Record<string, unknown> via a typed noteAsRecord alias.
  • ElectronAppWrapper.ts — 7 removed, 0 left. stateOptions: any → typed object literal; windowOptions: anyBrowserWindowConstructorOptions (uncovered that enableRemoteModule is no longer in the published WebPreferences; cast with comment explaining @electron/remote still relies on it); (event as any).isMainFrameas Electron.Event & {isMainFrame?: boolean}; close/open-url event handlers → import('electron').Event; ipcMain.on handlers → import('electron').IpcMainEvent with args typed unknown and cast at the assignment site. (Used import('electron').* inline rather than Electron.* because the project's lint config doesn't surface the Electron global.)
  • gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx — 4 removed, 3 left. commands: anyRecord<string, (...args: any[])=> unknown> (kept inner any with reason — commands are heterogeneous and dispatched by name); replaceSelection.value: any / insertText.value: anystring; onEditorPaste.event: any{ preventDefault } | null; loadScript.script: any / element: any{src; id?; attrs?} / HTMLScriptElement | HTMLLinkElement with branch-specific narrowing; options: any → typed object with optional percent. Two of the remaining disables collapsed into the new commands typing comment.
  • gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useJoplinMode.ts — 0 removed, 7 left. All CM5 mode/state/stream anys reason-updated (no @types/codemirror in this repo).
  • gui/NoteListItem/utils/useItemElement.ts — 7 removed, 0 left. All as any event-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: anyWindowControl; introduced local TagOption { value; label } for the suggestion arrays; sort/map callbacks typed via TagEntity; onClose(answer: any[])unknown with internal as TagOption[]. The value: startTags needed as unknown as string because DialogState's value is typed string.
  • gui/NotePropertiesDialog.tsx — 7 removed, 2 left. okButton: anyRefObject<HTMLButtonElement>; styles_: anyRecord<string, CSSProperties>; buttonRow_click(event: any){ buttonName: string }; editPropertyButtonClick.initialValue: anystring | number | null; (newFormNote as any)[k] and (formNote as any)[key]as unknown as Record<string, unknown> casts; editedValue: anystring | number | null. Two stay with descriptive reasons: latLongFromLocation output spreads into NoteEntity which expects number lat/long while the code keeps them as strings (runtime coercion); createNoteField.value is genuinely heterogeneous (timestamps + ids + urls).
  • gui/utils/NoteListUtils.ts — 0 removed, 11 left. All commandToStatefulMenuItem(...) as any casts 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).joplinas unknown as Record<string, unknown>. The reducer/middleware/Tesseract/menu callback/redux dispatch anys 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_: anyReact.CSSProperties; sidebar_selectionChange.event: any / renderSectionDescription.section: any / sectionToComponent.section: any/settings: anySettingMetadataSection + Record<string, unknown>; sectionWidths: Record<string, any>Record<string, string>; sectionStyle: anyReact.CSSProperties; needRestartComp: anyReact.ReactNode; mapStateToProps(state: any)AppState. Required settings['sync.target'] as number casts at call sites. Constructor props: any stays (matches the class's open props type).
  • services/plugins/PluginRunner.ts — 11 removed, 0 left. ipcRendererSend.args / eventHandler.argsunknown/unknown[]; PluginMessage.args/result/errorunknown[]/unknown; introduced local CallbackPromise interface and typed callbackPromises: Record<string, CallbackPromise>; mapEventIdsToHandlers.arg: anyunknown with internal as Record<string, unknown> for the object-iteration branch; ipcRenderer.on handler typed via IpcRendererEvent; result/error locals → unknown / Error | null with as Error in the catch.
  • gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollHandler.ts — 3 removed, 7 left. scrollTimeoutId_: any / restoreEditorPercentScrollTimeoutId_: anyReturnType<typeof setTimeout> | null; scheduleOnScroll.event: any{ percent: number }. The remaining seven CM5 cm/codeMirror: any entries reason-updated to mention the dynamic editor/scrollInfo types.
  • gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useListIdent.ts — 0 removed, 9 left. All CM5 plugin-loader anys 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: anyCSSProperties & { width?; height? }; State fields promptOptions/notePropertiesDialogOptions/noteContentPropertiesDialogOptions/shareNoteDialogOptionsRecord<string, unknown>; waitForNotesSavedIID_: anyReturnType<typeof setInterval>; ipcRenderer.on handler → (IpcRendererEvent, string, { url }); produce(layout, (draft: any))LayoutItem; layoutModeListenerKeyDown.event: anyKeyboardEvent; urlStyle: anyReact.CSSProperties; renderNotification.styles: anyRecord<string, CSSProperties>; resizableLayout_resize.event: any{ layout: LayoutItem }; resizableLayout_renderItem.event: any → typed shape (eventEmitter: EventEmitter; visible; size: Size; item: LayoutItem) — also added missing return null; and the explicit : React.ReactNode return type; components: anyRecord<string, ()=> React.ReactNode>; dispatch as anyas 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/options anys reason-updated; covers EditorProps heterogeneous callbacks (onChange/onScroll/onEditorPaste/onResize/onUpdate), the dynamic cmOptions record, and ref/wrapper signatures.
  • gui/NoteEditor/utils/types.ts — 12 removed, 5 left. NoteEditorProps: notes: any[]NoteEntity[]; editorNoteStatuses: anyRecord<string, string>; selectedNoteTags: any[]TagEntity[]; watchedResources: anyRecord<string, unknown>; highlightedWords: any[]string[]. NoteBodyEditorProps: style: anyReact.CSSProperties; onWillChange.event: any{ changeId: number }; noteToolbar: anyReact.ReactNode; dangling searchMarkers: any disable removed (already typed SearchMarkers). MessageEvent.args: any[] reason updated. bodyEditorContent: any reason updated (TinyMCE retains a raw editor object). ScrollOptions.value / OnChangeEvent.content / EditorCommand.value / CommandValue.args/value stay 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: any stay with descriptive reasons (DialogState.promptOptions has heterogeneous values per inputType). styles_: any stays (heterogeneous style blocks + react-select factories). answerInput_: any stays (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: anyunknown; onKeyDown.event: anyReact.KeyboardEvent. makeAnimated() as any casts kept inline with reasons.
  • gui/plugins/GotoAnything.tsx — 13 removed, 7 left. UserDataCallbackEvent.item: anyNoteEntity | FolderEntity | ResourceEntity | TagEntity; folders: any[]FolderEntity[]; Dialog: anyReact.ComponentType<Props>; manifest: any → typed object with menuItem shape; onTrigger.event: any{ userData }; inputRef/itemListRef: anyRefObject<HTMLInputElement> / RefObject<ItemList<...>>; input_onChange/listItem_onClick/input_onKeyDown.event: any → React event types; results: any[] / row: any / result: anyGotoAnythingSearchResult[] and result.id accesses; mergeOverlappingIntervals.f: any[number, number]; gotoItem.item: anyGotoAnythingSearchResult & { commandArgs? }; selectedItemIndex.results: any[]GotoAnythingSearchResult[]. Notes results from Tag.searchAllWithNotes/SearchEngine.search and the folder spread need as unknown as GotoAnythingSearchResult[] casts since lib's signatures are looser. styles_: any and the heterogeneous commandResults callback signature stay.
  • gui/MenuBar.tsx — 0 removed, 23 left. All 23 any usages reason-updated: Electron MenuItemConstructorOptions has 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_: anyReturnType<typeof setTimeout> | null. All 18 remaining anys 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. Combined github/array-foreach + no-explicit-any disable kept on one line (lint requires consecutive disable-next-line directives 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. Combined ban-types + no-explicit-any disable kept on one line; reason explains searches: any[] matches lib reducer and highlightedWords is 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:

  1. CodeMirror 5 dynamic loader / dynamic editor / scrollInfo / line-handle types — no @types/codemirror in 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.
  2. TinyMCE editor / event types — looser than @types/tinymce (we use APIs not in the published types). File: TinyMCE.tsx (18).
  3. Electron MenuItemConstructorOptions — heterogeneous shapes (submenu/role/type/click vary by item kind). Files: MenuBar.tsx (23), NoteListUtils.ts (11), app.ts (a few).
  4. react-select style/theme factories — library's own provided styles, tightening requires importing each StyleConfig generic. File: PromptDialog.tsx (9).
  5. 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.
  6. Legacy class components without props/state interfacesRoot.tsx, OneDriveLoginScreen.tsx, DropboxLoginScreen.tsx, ConfigScreen.tsx, PromptDialog.tsx.
  7. Heterogeneous editor commandsEditorCommand.value / CommandValue.args/value / ScrollOptions.value / OnChangeEvent.content — each editor dispatches different shapes by name.
  8. Library API mismatches — Electron's @electron/notarize types missing appBundleId; WebPreferences.enableRemoteModule removed; Electron's OpenDialogOptions.properties is a strict union but the app uses string[].
  9. Heterogeneous test fixtures / external library shapeselectron-context-menu's actions/props, PluginManager dynamic menu items, tesseract.js dynamic loader.
  10. CSS / stylingstyled-components.attrs typing 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. Typed action({ note, notebook? }).
  • app/command-mv.ts — 1 removed, 0 left. Typed action({ item, notebook }).
  • app/command-ren.ts — 1 removed, 0 left. Typed action({ item, name }).
  • app/command-rmbook.ts — 1 removed, 0 left. Typed action({ notebook, options? }).
  • app/command-rmnote.ts — 1 removed, 0 left. Typed action({ 'note-pattern', options? }).
  • app/command-restore.ts — 1 removed, 0 left. Typed action({ pattern }).
  • app/command-edit.ts — 1 removed, 0 left. Typed action({ note }).
  • app/command-import.ts — 1 removed, 0 left. Typed action({ path, notebook?, options }); outputFormat assignment now requires as ImportModuleOutputFormat.
  • app/command-mkbook.ts — 1 removed, 0 left. Typed action({ 'new-notebook', options }).
  • app/command-help.ts — 1 removed, 0 left. Typed action({ command? }).
  • app/command-use.ts — 1 removed, 0 left. Typed action({ notebook }).
  • app/command-attach.ts — 1 removed, 0 left. Typed action({ note, file }).
  • app/command-cat.ts — 1 removed, 0 left. Typed action({ note, options }).
  • app/command-geoloc.ts — 1 removed, 0 left. Typed action({ note }).
  • app/command-apidoc.ts — 1 removed, 0 left. Typed action({ file }).
  • app/command-done.ts — 2 removed, 0 left. Typed handleAction(args: { note }) and action(args: { note }).
  • app/command-set.ts — 2 removed, 0 left. Typed action(args: { note, name, value? }); newNote: anyRecord<string, unknown> (still accepted by Note.save).
  • app/command-ls.ts — 1 removed, 1 left. Typed action args; left queryOptions: any because it is fed to both Folder.all (FolderLoadOptions) and Note.previews (PreviewsOptions) and the union has fields that neither type carries (caseInsensitive, orderBy). Comment reason updated.
  • app/setupCommand.ts — 2 removed, 0 left. Typed cmd: BaseCommand, introduced local PromptOptions; the dispatcher arg is inferred from BaseCommand.setDispatcher(fn: DispatcherFn) (added in base-command.ts edit), eliminating the last any here. Side fix: App.setupCommand(cmd: string) was a wrong-pre-existing-annotation, now typed BaseCommand.
  • app/command-e2ee.ts — 2 removed, 0 left. Typed action args; typed askForMasterKey(error: { masterKeyId }).
  • app/command-config.ts — 3 removed, 0 left. chunks: anyBuffer[]; typed action args; Record<string, any>Record<string, unknown> for resultObj.
  • app/command-export.ts — 3 removed, 0 left. Typed action args; n.id map callbacks now infer entity types from loadItems; format assignment uses ExportModuleOutputFormat.Jex.
  • app/gui/FolderListWidget.ts — 3 removed, 0 left. Introduced local FolderListItem = FolderEntity | TagEntity | SearchItem | '-' for itemRenderer and newItems; replaced (this.folders[i] as any).note_count with FolderEntity & { note_count?: number }.
  • app/command-settingschema.ts — 4 removed, 0 left. Typed action args; Record<string, any>Record<string, unknown> for schema, props; removed unused v: any annotation.
  • app/gui/StatusBarWidget.ts — 2 removed, 2 left. Typed prompt(promptString: string, options) and textStyle: (s: string) => s. Left 2 any for tkwidgets terminal-kit inputField options/callback — no published types and tkwidgets has no .d.ts. Comments updated with reason.
  • app/LinkSelector.ts — 5 removed, 0 left. Introduced local TextWidget interface for tk text widget shape; replaced (lines[i] as any).matchAll with direct call (String.prototype.matchAll is well-typed).
  • app/command-sync.ts — 3 removed, 2 left. Typed action args, log(...s: string[]). Left options: any and report: any because Synchronizer.start(options: any) and Synchronizer.reportToLines(report: any) in lib are themselves any — 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][]; typed action args; 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 were Old code before rule was applied, so out of scope per rule 3. Side fix in checkpoint 1: App.setupCommand(cmd: string) annotation corrected to BaseCommand.
  • app/services/plugins/PluginRunner.ts — 6 removed, 0 left. wrapper: anyRecord<string, (...args: unknown[]) => unknown> with final cast to typeof Console to match SandboxProxy.console; arg arrays → unknown[]; activeSandboxCalls_: anyRecord<string, boolean>.
  • app/base-command.ts — 4 removed, 4 left. Introduced typed StdoutFn/PromptFn/DispatcherFn aliases; typed stdout_/prompt_/dispatcher_ fields; typed encryptionCheck(item: { encryption_applied? }). Left action(_args: any) and options(): unknown[]: parameters are contravariant so tightening base would break all subclass overrides. The 3 remaining any are 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: anyParseOptions (exported from @joplin/lib/HtmlToMd).
  • tests/MdToHtml.ts — 3 removed, 0 left. newTestMdToHtml(options: any)Partial<MdToHtmlConstructorOptions>; the ResourceModel mock requires as unknown as cast because it intentionally omits filename and isSupportedImageMimeType. mdToHtmlOptions already had a real type, just a stale disable comment. pluginOptions: anyRecord<string, { enabled: boolean }>.
  • tests/testUtils.ts — 1 removed, 0 left. Record<string, any>Record<string, unknown> on PluginServiceOptions.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 → inferred FolderEntity (from Folder.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 (both it blocks).

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.tsStdoutFn, PromptFn, DispatcherFn aliases at the top of the file (centralized) and the action(_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.ts queryOptions is passed to both Folder.all (FolderLoadOptions) and Note.previews (PreviewsOptions) and the union has fields neither type carries; splitting would be a structural refactor.
  • Loose lib-typed APIs (2): command-sync.tsSynchronizer.start(options: any) and Synchronizer.reportToLines(report: any) in lib are themselves any; tightening here would diverge from lib.
  • No published types for JS modules (3): StatusBarWidget (tkwidgets terminal-kit inputField options + callback) and command-sync.ts oneDriveApiUtils_ (onedrive-api-node-utils.js is plain JS).
  • Dynamic command/state shapes already documented (6): all of app.ts was 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 anynull as unknown as Synchronizer with type-only import.
  • components/shared/config/shouldShowMissingPasswordWarning.ts — 1 removed, 0 left. settings: anyRecord<string, unknown>.
  • folders-screen-utils.ts — 1 removed, 0 left. scheduleRefreshFoldersIID_: anyReturnType<typeof shim.setTimeout>.
  • geolocation-node.ts — 1 removed, 0 left. fetchJson return → Record<string, unknown> (and renamed shadowed let r to const response to 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: anyRefObject<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: anyRecord<string, { enabled: boolean }>.
  • models/Note.test.ts — 1 removed, 0 left. t[0] cast as NoteEntity instead of inner let input: any.
  • models/NoteResource.ts — 1 removed, 0 left. Introduced exported AssociatedResourceNote = Partial<NoteEntity> & { resource_id; note_id } to reflect the join shape.
  • models/Revision.test.ts — 1 removed, 0 left. input as anyinput as RevisionEntity.
  • models/Revision.ts — 0 removed, 1 left. Tried unknown[] for parsePatch; broke patchItem.diffs access. 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: anyError | null in 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: anyImportOptions.
  • services/interop/InteropService_Importer_Raw.test.ts — 1 removed, 0 left. Introduced local FolderEntityWithChildren interface; tree: any → typed cast.
  • services/interop/Module.test.ts — 1 removed, 0 left. format: ... as anyas ExportModuleOutputFormat.
  • services/interop/Module.ts — 1 removed, 0 left. format: '' as anyas ExportModuleOutputFormat.
  • services/joplinCloudUtils.ts — 1 removed, 0 left. Action.payload?: anystring (only ever holds errorMessage).
  • services/noteList/defaultMultiColumnsRenderer.ts — 0 removed, 1 left. Reason updated (matches OnRenderNoteHandler which is any by design; props heterogeneous per renderer's itemProps).
  • services/noteList/renderTemplate.test.ts — 1 removed, 0 left. name: '…' as anyas ColumnName.
  • services/noteList/renderViewProps.ts — 1 removed, 0 left. value: anyunknown with inner narrowing casts.
  • services/ocr/OcrService.ts — 1 removed, 0 left. maintenanceTimer_: anyReturnType<typeof shim.setInterval>.
  • services/plugins/MenuController.ts — 1 removed, 0 left. store: anyPluginStore (new exported alias from ViewController).
  • services/plugins/MenuItemController.ts — 1 removed, 0 left. Same fix via PluginStore.
  • services/plugins/ToolbarButtonController.ts — 1 removed, 0 left. Same fix via PluginStore.
  • services/plugins/ViewController.ts — 3 removed, 1 left. Introduced exported PluginStore = Store<any> (state heterogeneous across desktop/mobile — mainLayout lives on AppState only); typed store_/store/message. storeView stays any (controllers index different view shapes).
  • services/plugins/Plugin.ts — 4 removed, 0 left. Introduced exported MessageListenerCallback = (message: unknown)=> Promise<unknown>; typed messageListener_, contentScriptMessageListeners_, emitMessage, onMessage, onContentScriptMessage, emitContentScriptMessage.
  • services/plugins/RepositoryApi.ts — 1 removed, 0 left. (manifest as any)[field]as const tuple + direct indexing.
  • services/plugins/api/JoplinContentScripts.ts — 1 removed, 0 left. callback: anyMessageListenerCallback.
  • 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. postMessageHandler now async returning Promise<unknown>. The loadedModule.codeMirrorResources/codeMirrorOptions access kept as any with updated reason — these properties are not on ContentScriptModule.
  • services/interop/InteropService_Importer_EnexToHtml.ts — follow-up fix: outputFormat: 'html'ImportModuleOutputFormat.Html (caused by ImportOptions tightening).

Files skipped entirely:

  • database-driver.tsSelectResult = any already tagged "Partial refactor"; out of scope.
  • file-api-driver-local.ts — already tagged "Partial refactor".
  • services/interop/InteropService_Importer_Md_frontmatter.tsparseRawYamlToFolderIcon already 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): anyunknown (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 to unknown forces narrowing at every branch and recursive call).
  • services/plugins/utils/validatePluginPlatforms.test.ts — 1 removed, 0 left. platforms: anyunknown; inner cast in call.
  • services/profileConfig/index.ts — 1 removed, 0 left. Introduced local MigratingProfile/MigratingProfileConfig interfaces describing the v1→v2 transition.
  • services/rest/ApiResponse.ts — 1 removed, 0 left. body: anyunknown.
  • 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: anyLoadOptions.
  • services/rest/utils/defaultLoadOptions.ts — 1 removed, 0 left. Return → LoadOptions.
  • services/rest/utils/defaultSaveOptions.ts — 1 removed, 0 left. Introduced exported DefaultSaveOptions { userSideValidation; isNew?; autoTimestamp? }; callers set autoTimestamp after construction.
  • services/search/SearchFilter.test.ts — 1 removed, 0 left. let engine: anySearchEngine.
  • 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: anyError | null.
  • services/synchronizer/gui/useSyncTargetUpgrade.ts — 1 removed, 0 left. error: anyError | null.
  • services/synchronizer/migrations/1.ts and 2.ts — 2 removed, 0 left. api: anyFileApi.
  • services/synchronizer/utils/handleConflictAction.ts — 1 removed, 0 left. remoteContent/local: anyBaseItemEntity.
  • 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. OnAfterCallbackCreated callback 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 with null as unknown for cache fields.
  • ArrayUtils.ts — 2 removed, 0 left. mergeOverlappingIntervals typed [number, number][]; one caller (GotoAnything.tsx) annotated its indices accordingly.
  • JoplinError.ts — 2 removed, 0 left. Introduced exported JoplinErrorCode = string | number | null.
  • ObjectUtils.ts — 2 removed, 0 left. output: any in sortByValue and convertValuesToFunctionsRecord<string, ...> with final cast to the typed return type.
  • dom.ts — 2 removed, 0 left. isInsideContainer(node: any)EventTarget | Node | null; waitForElement made generic <T extends HTMLElement> returning T | null. One caller (useRootElement.ts) now uses the generic explicitly.
  • errorUtils.ts — 2 removed, 0 left. Introduced local WrapErrorInput and WrappedError interfaces.
  • net-utils.ts — 2 removed, 0 left. Introduced TcpPortUsed { 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: anyRecord<string, unknown>; added SaveOptions.fields?: string[] (used by BaseModel.save and at least one renameTag caller).
  • models/Alarm.ts — 2 removed, 0 left. selectAll cast to { id: string }[]; makeNotification(alarm, note)AlarmEntity/NoteEntity.
  • models/Tag.ts — 2 removed, 0 left. searchAllWithNotes(options: any)SearchOptions (new exported interface in BaseModel); save.options: anySaveOptions.
  • models/utils/readOnly.ts — 2 removed, 0 left. Folder: anytypeof import('../Folder').default; BaseItem: anytypeof import('../BaseItem').default.
  • components/shared/config/config-shared.ts — 1 removed, 1 left. updateSettingValue.value: anyunknown. The setState field is left any with a new reason — mirrors React.Component.setState (Pick<S, K>); narrowing breaks subclass this assignment to the interface in app-mobile's class-based ConfigScreen.
  • components/shared/config/plugins/useOnInstallHandler.ts — 2 removed, 0 left. Both setInstallingPluginIds((prev: any)) callbacks rely on the React.Dispatch<SetStateAction<...>> inferred type.
  • components/shared/reduxSharedMiddleware.ts — 1 removed, 1 left. sortNoteListTimeout: anyReturnType<typeof shim.setTimeout>; store/_next typed (Store<State>/Dispatch); action: any kept 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 — call waitForElement<HTMLDivElement>(...) explicitly.
  • app-desktop/plugins/GotoAnything.tsx — annotated local indices: [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. Local ElectronClipboardLike / ElectronNativeImageLike interfaces (electron module not available in non-desktop packages).
  • services/plugins/api/JoplinViews.ts — 3 removed, 0 left. implementation now BasePlatformImplementation.JoplinViews (the actual sub-object passed in); storePluginStore.
  • services/plugins/api/JoplinViewsDialogs.ts — 3 removed, 0 left. Introduced ShowOpenDialogOptions in BasePlatformImplementation; tightened return to string[] | null.
  • services/plugins/api/JoplinViewsEditor.ts — 4 removed, 0 left. storePluginStore; onMessage.callback: FunctionMessageListenerCallback; postMessage.message: anyunknown.
  • services/plugins/api/JoplinViewsPanels.ts — 3 removed, 0 left. Same pattern (PluginStore / MessageListenerCallback / unknown).
  • services/plugins/api/JoplinViewsToolbarButtons.ts — 3 removed, 0 left. storePluginStore; deprecation cast typed.
  • services/plugins/api/JoplinViewsMenus.ts — 4 removed, 0 left. storePluginStore; 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: cast implementation.clipboard/.nativeImage to the JoplinClipboard constructor parameter shapes.
  • services/plugins/BasePlatformImplementation.ts — 4 removed, 0 left. Introduced ShowOpenDialogOptions; clipboard/nativeImage/registerComponent returns/params → unknown.
  • services/plugins/api/noteListType.ts — 1 removed, 2 left. OnChangeEvent.value: anyunknown. RenderNoteView and OnRenderNoteHandler.props kept any with updated reasons (heterogeneous per-renderer shape).

Top-level lib files:

  • eventManager.ts — 0 removed, 3 left. Reason updated on filterEmit.object: any (filter objects vary per filter name). Two Partial refactor reasons already in scope; no change.
  • hooks/useEventListener.ts — 4 removed, 0 left. Introduced local EventHandler = (event: Event) => void; typed element as RefObject<EventTarget | null> (type-only import from react).
  • services/AlarmService.ts — 3 removed, 0 left. Introduced exported AlarmServiceDriver interface; updateNoteNotification.noteOrId typed NoteEntity | string.
  • models/Alarm.ts — follow-up: selectAll cast updated to { id: number }[] (alarm IDs are integers, unlike note IDs); batchDelete call now casts the resulting number[] (the function's signature accepts string[] for note-style IDs).
  • services/KvStore.ts — 4 removed, 0 left. Introduced JoplinDatabase and MutexInterface imports for typed fields; formatValues_ callers cast the Row[] results to KvStoreKeyValue[].

Checkpoint 4 (2026-05-14): 1004 → 977 (27 removed).

  • TaskQueue.ts — 3 removed, 0 left. TaskCallback/TaskResult.result/completeTask.resultunknown.
  • HtmlToMd.ts — 3 removed, 0 left. turndownOpts: anyRecord<string, unknown>; blankReplacement/replacement callbacks typed with HTMLElement.
  • InMemoryCache.ts — 3 removed, 0 left. Record.value/value()/setValue()unknown.
  • SyncTargetOneDrive.ts — 2 removed, 1 left. api_: anyOneDriveApi; (a: any)unknown. Left the inherited db/options constructor params (BaseSyncTarget still uses any there).
  • htmlUtils.ts — 3 removed, 5 left (4 of the 5 are existing ban-types Function disables and now-typed callbacks via new ReplaceUrlCallback / ProcessImageTagCallback aliases). attributesHtml.attr: anyRecord<string, string>; headAndBodyHtml.doc: anyDocument. One test (htmlUtils2.test.ts) gets a typed cast. The Function-typed processImageTags/replaceImageUrls/replaceEmbedUrls/replaceMediaUrls callbacks are now typed via new aliases — the ban-types disables go away with the explicit-any ones.
  • downloadController.ts — 3 removed, 0 left. Introduced local DownloadRequest / DownloadChunk interfaces.
  • services/interop/InteropService_Exporter_Base.ts — 3 removed, 1 left. prepareForProcessingItemType.itemsToExport: any[]BaseItemEntity[]; processItem.item: anyBaseItemEntity; processResource.resource: anyResourceEntity; updateContext.context: anyobject. The context_ field stays any with updated reason — shape is exporter-specific (Html has cssStrings/customAssets, Md has noteTags/tagTitles) and indexed dynamically by subclasses.
  • services/interop/InteropService_Exporter_Jex.ts — 3 removed, 0 left. processItem/processResource now match the tightened base; readDirStats.filter/map callbacks no longer need a cast.
  • services/interop/InteropService_Exporter_Html.ts — 3 removed, 0 left. style_: anyThemeStyle; init.options: anyExportOptions; processItem.item: anyNoteEntity.

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 using ShareInvitation/MasterKeyEntity; store_: Store<any>Store<unknown>; state getter narrows via as Record<string, unknown>.
  • services/ExternalEditWatcher.ts — 5 removed, 3 left. Introduced local DispatchFn/BridgeFn; eventEmitter_ typed via EventEmitter (switched to ES import). chokidar_/watcher_ stay any with a single shared reason (chokidar typings vary across platforms; we use a small subset structurally). on/off callbacks stay any with 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, indexed styles record); buildStyle.cacheKey: anystring | (string | number)[]. BuildStyleCallback return stays any (heterogeneous: CSSProperties, styled-components objects, plain CSS strings).
  • services/share/reducer.ts — 2 removed, 1 left. parseShareCache.raw: anyPartial<State>. The reducer's action: any keeps a reason about heterogeneous SHARE_* action shapes. (draft.shareUsers as any) cast removed.
  • services/interop/types.ts — 2 removed, 1 left. ImportOptions.destinationFolder: anyFolderEntity; onError: (error: any)Error. onProgress stays any with 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 dispatch action typed via Plugin.PluginDispatchCallback; readDirStats filter/map untyped. The store_ and platformImplementation_ fields stay any with 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 exported PluginDispatchCallback; dispatch_/constructor dispatch parameter typed; eventEmitter_: anyInstanceType<typeof EventEmitter>.

Checkpoint 6 (2026-05-14): 965 → 949 (16 removed).

  • services/rest/utils/collectionToPaginatedResults.ts — 3 removed, 1 left. items callbacks and sort comparator typed; the outer items: 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 local NativeImageLike interface (toPNG/toJPEG/resize/crop/getSize); cacheImage and Image.data typed; toJpgResource/toPngResource.resourcePropsPartial<ResourceEntity>.
  • testing/syncTargetUtils.ts — 4 removed, 0 left. Introduced local TestDataNode / TestData types for the recursive test data structure.
  • time.ts — 5 removed, 1 left. formatLocalToMs/anythingToDateTime/anythingToMs/goBackInTime/goForwardInTime typed using string | number | Date | { toDate } unions; added type-only MomentTypes import for unitOfTime namespace. msleep switched from Promise((resolve: Function)) to Promise<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. storePluginStore; onNoteChange wrapper event typed; selectedNote(): Promise<any>Promise<NoteEntity | null>. Two left (one is "No plugin-api-accessible Note type defined" reason already; one is the Function ban-types in onNoteSelectionChange).
  • services/PostMessageService.ts — 4 removed, 1 left. MessageResponse.response/.errorunknown / Error | null; Message.content and sendResponse.responseContentunknown. ViewMessageHandler left any with 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_ typed Record<string, Record<string, string[]>>; localeStats_ typed Record<string, Record<string, unknown>> (per-locale stats include pluralForms function); _/_n/stringByLocale rest args → unknown[]. Single inner cast as ParsePluralFormFunction for the plural-forms field.
  • services/plugins/reducer.ts — 3 removed, 2 left. (view as any) casts on PLUGIN_VIEW_PROP_SET/_PUSHas unknown as Record<string, unknown[]> etc. The reducer's action: any keeps a reason (heterogeneous PLUGIN_* action shapes). viewsByType returns any[] with reason (menu views have menuItems not on PluginViewState).
  • JoplinDatabase.ts — 4 removed, 2 left. TableField.default: anystring | number | boolean | null; tableDescriptions_: anyRecord<string, Record<string, string>>; open.options: anyRecord<string, unknown>; tableFields.options: any{ includeDescription?: boolean }; createDefaultRow.row: anyRecord<string, unknown>. constructor(driver: any) kept with reason — base Database.driver is any across 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: anyJoplinDatabase; WhereQuery.params(string|number|boolean)[]. The items: 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, newValues typed unknown / Record<string, unknown>. The ValidationHandler type alias keeps any with a new reason — settings are heterogeneous; each validator narrows from this base.
  • services/DecryptionWorker.ts — 5 removed, 0 left. Introduced local DecryptionWorkerStartOptions interface; dispatchReport.report: anyRecord<string, unknown>; dispatch: Function(action: { type; ... }); on/off callbacks updated reasons (heterogeneous payloads by event name).
  • models/Folder.test.ts — 3 removed, 0 left. foldersById: anyRecord<string, FolderEntity & { note_count?: number }>.
  • services/search/SearchEngineUtils.test.ts — 3 removed, 0 left. searchEngine: anySearchEngine; options: anyNotesForQueryOptions.
  • services/search/SearchEngine.test.ts — 4 removed, 0 left. Introduced local ExpectedTerms interface; helper extractValue narrows string | { value: string }.
  • services/share/ShareService.test.ts — 5 removed, 0 left. extraExecHandlers callbacks typed; per-handler body casts to specific shapes; Function ban-types disable goes away with the explicit-any ones.
  • services/ExternalEditWatcher/utils.ts — 4 removed, 0 left. spawnCommand.options: anySpawnOptions from child_process; wrapError.error: anyError | null; subProcess.on('error') callback typed Error; introduced local ExternalBridge { openItem } interface.
  • services/interop/InteropService_Importer_Raw.ts — 4 removed, 0 left. itemIdMap/createdResources: anyRecord<string, string> / Record<string, ResourceEntity>; folderExists.stats: any[]Stat[]; defaultFolder_: anyFolderEntity | null.
  • services/plugins/ViewController.ts — 2 removed, 2 left. emitMessage returns Promise<unknown>, postMessage.message: anyunknown. 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: anyRecord<string, string>; evaluate/validate.context: anyobject (matches existing callers that pass WhenClauseContext); createContext.getValue uses generic <T> to match IContext.getValue<T>.
  • services/plugins/api/JoplinData.ts — 6 removed, 0 left. api_: anyApi; serializeApiBody.body: anyunknown; route calls typed with RequestMethod enum and RequestFile[].

Checkpoint 9 (2026-05-14): 886 → 829 (57 removed).

  • models/settings/types.ts — 3 removed, 3 left. value: any keeps a new reason (settings heterogeneous, each consumer narrows); options/show left any with reasons (varying per setting/heterogeneous setting access). unitLabel/filter left where parameters are contravariant per-setting.
  • services/plugins/utils/executeSandboxCall.ts — 4 removed, 2 left. EventHandler.args: any[]unknown[]; nested args: any[]unknown[]. Recursive walker arg and dotted-path parent/fn left any with new reasons (heterogeneous sandbox object shape).
  • services/interop/InteropService_Exporter_Custom.ts — 5 removed, 1 left. Each CustomImporter method typed: context: ExportContext, item: BaseItemEntity, resource: ResourceEntity.
  • services/debug/populateDatabase.ts — 6 removed, 0 left. randomIndex/randomElement/randomElements made generic <T>; db: anyJoplinDatabase & { clearForTesting }; folder/note: anyFolderEntity / NoteEntity.
  • database-driver-better-sqlite.ts — 6 removed, 0 left. Introduced local BetterSqliteDatabase / PreparedStatement / SqliteError / WrappedError / SqlParams types.
  • fs-driver-base.ts — 5 removed, 3 left. Introduced exported FileHandle = unknown (drivers use different concrete shapes) and TarOptions { strict?, portable?, file, cwd }. appendFile/open/close/readFileChunk* typed. ReadDirStatsOptions.recursive made optional. readFile stays any with reason — widening to string|Buffer cascades broken call sites across many lib files.
  • fs-driver-node.ts — 9 removed, 0 left. Introduced FsError/WrappedFsError; fsErrorToJsError_, setTimestamp, readDirStats, open, close, readFileChunk, tarExtract, tarCreate typed.
  • services/AlarmServiceDriverNode.ts — 7 removed, 0 left. Introduced StoredNotification (extends Notification with timeoutId); notifications_/service_/setService typed; displayDefault/electron notification options typed with proper interface; notifier.notify callback typed.
  • services/plugins/WebviewController.ts — 6 removed, 3 left. Introduced local LayoutItem for findItemByKey; CloseResponse.resolve/reject typed; messageListener_/onMessage use MessageListenerCallback; storePluginStore; postMessage/setStoreProp value typed unknown.
  • registry.ts — 9 removed, 2 left. Various private fields typed (scheduleSyncId_/recurrentSyncId_/db_/showErrorMessageBoxHandler_); setShowErrorMessageBoxHandler/setDb/saveContextHandler typed; promiseResolve typed. syncTargets_/scheduleSync.syncOptions keep any with reasons (heterogeneous sync target API and Synchronizer.start options).
  • Side fix: app-cli/app/command-apidoc.ts — cast tableFields to MarkdownTableRow[] (now that TableField no 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.stateunknown; dispatch: Function removed (no usage). Two on/off callbacks stay any with reason (EventEmitter payloads vary). Follow-up in app-mobile/.../Note.tsx casts the undo state to its concrete shape.
  • JoplinServerApi.ts — 5 removed, 3 left. connectionErrorMessage.errorError | null; requestToCurl_.options typed inline; exec/exec_.query/headers typed Record<string, unknown> / Record<string, string>; responseJson_Record<string, unknown>. Three stay with reasons — body and fetchOptions.body flow through shim.fetch/fetchBlob/uploadBlob (FetchOptions) which type body as string; hidePasswords accepts both stringified bodies and header records; response is the shim/blob return.
  • services/ResourceFetcher.ts — 9 removed, 4 left. dispatch: Function typed via the action shape; queue_/autoAddResourcesCalls_ typed; timer IDs typed via ReturnType<typeof shim.setTimeout>; on/off reasons updated; the as any cast on notifyDisabledSyncItems callback replaced with a typed adapter. Four stay with reasons (fetchingItems_ mixed bool/Entity; fileApi_/setFileApi/constructor widened 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_.mdRecord<string, unknown>; output.type_ cast uses NoteEntity & { type_? }.
  • BaseSyncTarget.ts — 8 removed, 2 left. dispatch: Function typed via action shape; initState_/options_ typed; option/unsupportedPlatforms/checkConfig typed. 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_: anyFsDriverBase; fetchStatuses return → typed shape; markupTag.resource typed ResourceEntity & { alt? }; localState/setLocalStateQueries/setLocalState.resourceOrIdResourceEntity | string; itemCanBeEncrypted cast uses Parameters<...>; params: any[](string|number)[]; resourceConflictFolder return type inferred.
  • models/Folder.ts — 7 removed, 0 left. fieldsToLabels: anyRecord<string, string>; tableNameToClasses: Record<string, any>Record<string, typeof BaseItem> (2 places); handleTitleNaturalSorting.options typed; allAsTree.optionsFolderLoadOptions & { includeNotes? }; idToFolders.anyFolderEntityWithChildren; save.options: anySaveOptions & {duplicateCheck?, reservedTitleCheck?, stripLeftSlashes?}.

Checkpoint 13 (2026-05-14): 789 → 777 (12 removed).

  • WebDavApi.ts — 4 removed, 5 left. RequestInfo.options typed via FetchOptions & ...; _requestToCurl.options typed; serializeRequest callback body uses Record<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.isDir added (used by list filter); requestCanBeRepeated.error typed; tryAndRepeat made generic <T> with Function()=> T|Promise<T>; list filter callbacks typed; put.content: anystring | Buffer | null; multiPut.options: any{ source? }; introduced BasicDeltaContext and typed the helper; getDirStatFn: Function(path) => ItemStat[]|Promise<ItemStat[]>; sort/map callbacks typed. Five stay with reasons: RemoteItem.jopItem, PaginatedList.context, driver_, constructor driver, and the output: 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_/dispatch typed; eventEmitter_ typed InstanceType<typeof EventEmitter> with proper ES import; externalApi.openAndWatch/watch/stopWatching/isWatched callbacks 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. Typed metadataToStat_/metadataToStats_ arguments; introduced RemoteItem return type (added id: '' to satisfy the interface); typed delta/list/get/put/multiPut options via ExecOptions from JoplinServerApi; typed isRejectedBySyncTargetError/isReadyOnlyError.error; typed Object.entries<...> response shape. Also caught a typo: response.has_moreresponse.hasMore (the returned object actually exposes hasMore).

Checkpoint 15 (2026-05-14): 756 → 732 (24 removed).

  • ClipperServer.ts — 11 removed, 2 left. Introduced local ClipperDispatch alias for the dispatch field; server_ typed http.Server & { destroy? } (server-destroy adds .destroy at runtime); all request/response handler callbacks typed via IncomingMessage/ServerResponse; writeCorsHeaders/writeResponseJson/writeResponseText/writeResponseInstance/writeResponse/execRequest typed; multiparty.Form.parse callback typed; request.on('data') typed Buffer | string. Two stay with reasons (actionApi shape varies, the Api route method parameter cast).
  • testing/test-utils.ts — 13 removed, 2 left. Typed switchClient/setupDatabase/setupDatabaseAndSynchronizer options bag; introduced FolderTreeNode for createFolderTree; objectsEqualRecord<string, unknown>; checkThrowAsync/expectThrow/expectNotThrow/checkThrow → function signatures; id/ids/sortedIds{ id?: string }; at<T> made generic; createNTestNotes.folderFolderEntity; middlewareCalls_: any[]boolean[]; start.argvstring[]. Two stay (synchronizerStart.extraOptions and generalMiddleware — Synchronizer.start signature and BaseApplication.generalMiddleware override force any).

Checkpoint 16 (2026-05-14): 732 → 700 (32 removed).

  • file-api-driver-memory.ts — 6 removed, 0 left. Introduced local MemoryItem interface for items_/deletedItems_; encodeContent_/decodeContent_ typed string | Buffer / string; setTimestamp return Promise<void>; get/put/multiPut/delta options typed via GetOptions/PutOptions/DeltaOptions; multiPut.output typed.
  • eventManager.ts — 4 removed, 0 left. AppStateChangeCallback made generic <T>; FilterHandler made generic <T> (matches plugin api/types FilterHandler<T>); filterEmit/filterOn/filterOff/appStateOn/appStateOff all generic with internal unknown-cast for storage; stateValue_ uses Record<string, unknown> instead of any.
  • markdownUtils.ts — 1 removed, 0 left. prependBaseUrl replace callback _match: anystring.
  • utils/focusHandler.ts — 2 removed, 0 left. Adopted unknown for element arg with a MaybeFocusable cast inside toggleFocus so the runtime typeof check narrows the call. Many call sites pass things like Element/EventTarget/EditorView that have only focus, so any typed interface would have rejected them.
  • utils/joplinCloud/index.ts — 2 removed, 0 left. Extended PlanFeature with missing basicInfo/proInfo/teamsInfo/joplinServerBusinessInfo* keys (used at call sites but undeclared); getFeatureLabel/getCellInfo use keyof PlanFeature indexing with typeof v === 'string' guards.
  • utils/frontMatter.ts — 3 removed, 0 left. toLowerCaseRecord<string, unknown>; parse added local asString/asNumber coercion helpers since yaml.load returns unknown-shaped values; 'tags' in md check now also requires Array.isArray before assigning.
  • utils/ipc/RemoteMessenger.ts — 9 removed, 0 left. Replaced ambient FinalizationRegistry constructor/register any with (id: string)=> void and object; Proxy get returns unknown; canRemoteAccessProperty.parentObject/trackCallbackFinalization.callback/methodFromPath parent/current/stack typed unknown with narrowing Record<string, unknown> casts where indexing is required.
  • file-api-driver-local.ts — 1 removed, 0 left. fsErrorToJsError_.output typed Error & { code?: JoplinError['code'] } instead of any.
  • SyncTargetOneDrive.ts — 1 removed, 0 left. Constructor options: anyRecord<string, unknown> (matches BaseSyncTarget); db: any keeps reason via // eslint-disable-next-line referencing BaseSyncTarget.
  • file-api.ts — 1 removed, 0 left. Introduced ListOptions interface ({ includeHidden?, includeDirs?, syncItemsOnly?, context? }); FileApi.list.options: anyListOptions.
  • database-driver.ts — 1 removed, 0 left. SelectResult = anyunknown with a comment explaining narrowing happens at the model layer.
  • database.ts — 1 removed, 8 left with updated reasons. Tried widening Row/selectAllFields/open/insertQuery/updateQuery/formatValue to unknown but each cascaded into many downstream errors across JoplinDatabase, models/*, services/KvStore, services/RevisionService, etc. (typically requiring index-signature errors, or explicit narrowing for already-extant column accesses). Reverted to any with descriptive reasons. The one removed was enumId using (this as unknown as Record<string, number>) for the dynamic TYPE_* lookup. wrapQueries/wrapQuery were already widened safely ((string | SqlQuery | [string, SqlParams?])[]).
  • services/synchronizer/syncInfoUtils.ts — 9 removed, 1 left. masterKeys/masterKeyMap typed MasterKeyEntity[]/Record<string, MasterKeyEntity>; fetchSyncInfo.output: any{ version: number; [k: string]: unknown }; toObject return type inferred; setWithTimestamp/keyTimestamp/setKeyTimestamp all use (this as unknown as Record<...>) casts. The load.s: any stays — JSON parse output varies per sync target version and is validated per-field via 'x' in s checks below.

Checkpoint 17 (2026-05-14): 700 → 681 (19 removed).

  • services/CommandService.ts — 5 removed, 7 left. CommandContext.dispatch: Function kept (acts as a variance escape hatch — desktop runtimes type dispatch more strictly via DesktopCommandContext). ReduxStore simplified to type ReduxStore = any with a reason — tests and platforms pass partials and add subscribe etc. scheduleExecute.args: anyunknown. componentUnregisterCommands.commands kept ComponentCommandSpec<any> with updated reason. CommandRuntime.execute and Api.execute kept any with updated reasons (per-command/per-route shapes). createContext.dispatch.action typed unknown.
  • services/rest/Api.ts — 7 removed, 5 left. Request.params: any[]string[]; Request.action?: anystring. RequestContext.dispatch: Function → typed dispatch. RouteFunction reason updated. Api.token_: string | Functionstring | (()=> string); knownNounces_: anyRecord<string, string>; dispatch_: Function → typed dispatch. Constructor and private dispatch typed. actionApi_ stays any with reason. execServiceActionFromRequest_.externalApi: any → typed. Request.body/bodyJson_/bodyJson/Api.route keep any with route-specific reasons.
  • services/interop/InteropService.ts — 4 removed, 2 left. eventEmitter_: anyEventEmitter (changed require → ES import { EventEmitter } from 'events'); on/off.callback: Function → typed; context: any{ resourcePaths; destResourcePaths?; notePaths? }. normalizeItemForExport made generic <T extends Record<string, unknown>>; override: anyPartial<{ is_shared; share_id }>. itemsToExport: any[]/queueExportItem.itemOrId: any kept any with updated reasons (mirror exporter signatures across all subclasses; structural { type, itemOrId }).
  • services/interop/InteropService_Exporter_Md.ts — 5 removed, 1 left. makeDirPath_.itemNoteEntity | FolderEntity; replaceLinkedItemIdsByRelativePaths_.itemNoteEntity; replaceItemIdsByRelativePaths_.paths/fn_createRelativePath typed; prepareForProcessingItemType context inner type narrowed; processItem.itemNoteEntity | FolderEntity. The outer prepareForProcessingItemType.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 same itemsToExport: any[] / queueExportItem boilerplate appeared 10 times; replaced all with const { items: itemsToExport, queue: queueExportItem } = createExportItems(); (the helper already existed at the top of the file). One inline context: any → typed shape. Resource.load call sites cast itemOrId as string.
  • services/interop/InteropService.test.ts — 11 removed, 0 left. fieldsEqual made generic <T extends object>; Item.object: anyunknown; export callback signatures typed via BaseItemEntity/ResourceEntity; Folder.all result typed; result: any typed inline; format: 'testing' as anyas ExportModuleOutputFormat.
  • services/synchronizer/synchronizer_MigrationHandler.test.ts — 10 removed, 0 left. MigrationTests.[key]: Function()=> Promise<void>; the 10 items.filter((i: any) => ...) callbacks rely on RemoteItem inference now.
  • services/synchronizer/ItemUploader.test.ts — 6 removed, 0 left. Introduced MultiPutItem / MultiPutResult / ItemBodyCallback aliases; ApiCall.args: any[]unknown[]; clearArray made generic; newFakeApi/newFakeApiCall typed; the args[0].length accesses now cast via MultiPutItem[].
  • services/rest/routes/notes.ts — 13 removed, 0 left. Introduced Stylesheet, ImageSize, exported ImageSizes. htmlToMdParser_HtmlToMd; RequestNote.id: anystring, anchor_names: any[]string[], stylesheets: anyStylesheet[], image_sizesImageSizes. requestNoteToNote.output: anyNoteEntity. tryToGuessExtFromMimeType.response: any → headers shape. replaceUrlsByResources.imageSizes typed; the replace callback _match: anystring. attachImageFromDataUrl.note: anyNoteEntity, cropRect: any typed. extractNoteFromHTML.imageSizes typed.

Checkpoint 19 (2026-05-14): 620 → 574 (46 removed).

  • models/settings/builtInMetadata.ts — 28 removed, 0 left. The 20 show: (settings: any) callbacks all inherit the type from SettingItem.show?(settings: any); dropping the explicit annotation removes the disable comments while keeping the interface as-is. themeOptions / multiple options() callbacks : anyRecord<string|number, string>; filter: (value: any) → infers from interface; 'notes.sharedSortOrder' value cast to Record<string, unknown>. Side fix: services/sortOrder/PerFolderSortOrderService.ts widens the read-back to { field?: string; reverse?: boolean } & Record<...>.
  • Synchronizer.ts — 18 removed, 4 left. Introduced local ProgressReport interface (errors, state?, startTime?, completedTime?, counter index signature) typed across progressReport_/reportHasErrors/completionTime/reportToLines/logSyncSummary. isCannotSyncError.error: any typed; downloadQueue_: anyTaskQueue (constructor now passes logger via TaskQueue's second arg instead of mutating a private field); setEncryptionService.v: anyEncryptionService; logSyncOperation.local typed { id?, path?, type_? }. Several lambda callbacks typed: startAutoLockRefresh.error: anyError; result.items.filter((it: any)) infers from itemsThatNeedSync. local = resource as any → cast through typeof local. (action: any) => dispatch(action) typings dropped (infer). Four stay with reasons: apiCall dispatch by name (FileApi heterogeneous methods), start.options: any (caller bag), handleCannotSyncItem.item: any (BaseItem.saveSyncDisabled), and the BaseItem.save options bag during DELTA. Side fix: cancel() now uses void 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 anyas AppType; saveTimeoutId_/changeEventTimeoutId_: anyReturnType<typeof shim.setTimeout>; rows.map((r: any)) typed inline; toPlainObject.keyToValues: anyRecord<string, unknown>; incValue.inc: anynumber; setArrayValue.settingValue: any[]string[]; objectValue/setObjectValue.value: anyunknown; enumOptionLabel/isAllowedEnumOption.value: anyunknown/string; subValues.output: anyRecord<string, unknown>; groupMetadatasBySections.{generalSection,nameToSections}: anySettingMetadataSection/Record<string, SettingMetadataSection>. The 7 that stay (with reasons): SettingValueType fallback, CacheItem.value, keychainService_ and its setter (concrete class in app-desktop/app-mobile), valueToString/formatValue (heterogeneous values per setting type), and the constants_ lookup in value(). Side fix: markupLanguageUtils.ts now coerces with !! for pluginOptions.enabled.
  • models/Note.ts — 11 removed, 13 left. PreviewsOptions.conditionsParams: any[](string|number|boolean)[]; geolocationCache_/dueDateObjects_: any → typed shapes. linkedItemIds.links typed { itemId: string }[]. replaceResource{Internal,External}ToInternalLinks.options: any{ useAbsolutePaths? }. sortNotes.orders: any[]{ by: string; dir: string }[]; noteFieldComp made generic; sort prop access typed string|number|boolean. previewFields.options: any{ includeTimestamps? }. loadFolderNoteByField.value: anystring|number|boolean. preview.options: any{ fields?: string|string[]; excludeConflicts? }. Introduced DuplicateOptions for duplicateMultipleNotes/duplicate. The 13 that stay are mostly around the dynamic (newNote as any)[field] patterns retained as Record<string, unknown> casts and search.options forwarding to BaseItem.search. Side fixes: models/Note.test.ts testCases typed [boolean, string, string][].

Checkpoint 21 (2026-05-14): 539 → 506 (33 removed).

  • shim.ts — 11 removed, 25 left. msleep_ resolver typed Promise<void> (no Function). isElectron window/process casts use structural types instead of any. fetchRequestCanBeRetried.error: any{ code?, message? }. fetchText.optionsFetchOptions. fetchBlob/uploadBlob/imageFromDataUrl keep any with descriptive reasons. Throwing stub methods now have concrete return types where unambiguous (stringByteLength: number, appVersion: string, pathRelativeToCwd: string, writeImageToFile.format: string, setReact/setReactDom typed via typeof React). Several stay any for genuine cross-platform polymorphism: Geolocation, electronBridge_, fsDriver_, httpAgent_, proxyAgent, nodeSqlite_, sjclModule, the node datagram module, requireDynamic, fetchWithRetry.fetchFn, setTimeout/setInterval return types, openUrl (boolean vs Promise), readLocalFileBase64 (string vs Promise).
  • BaseApplication.ts — 10 removed, 9 left. eventEmitter_: anyEventEmitter (changed require → ES import); scheduleAutoAddResourcesIID_ typed via ReturnType<typeof shim.setTimeout>; currentFolder_: anyFolderEntity. on(callback: Function) → typed; switchCurrentFolder.folderFolderEntity; refreshNotes.state: anyState (with parentType: string|number to allow reassign to ModelType numeric); resourceFetcher_downloadComplete.event typed; reducerActionToString.action typed { type; [k]: unknown }; applySettingsSideEffects.action/sideEffects typed; readFlagsFromFile.flags: any replaced with flagArgs: 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. Introduced OneDriveAuth interface and ListenerCallback type. auth_/setAuth/auth() typed; accountProperties_Record<string, unknown>; listeners_: Record<string, ListenerCallback[]>; dispatch.param/setAccountProperties typed; oneDriveErrorResponseToError.errorResponse typed (return still any since downstream augments with request/headers/body fields); execTokenRequest.body/refreshAccessToken.bodyRecord<string, string>; authorizationTokenRemoved.data typed unknown with Record<string, unknown> inner cast. The 7 remaining (uploadChunk, uploadBigFile, exec, execJson, execText, handleRequestRepeat.error, oneDriveErrorResponseToError return) 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. activeMasterKeyId and loadedMasterKey error throws now use Error & { code; masterKeyId? } casts instead of any. wrapSjclError.sjclError typed. randomHexString/generateMasterKeyContent_ cast shim.randomBytes to number[] at use site. stringWriter_ typed inline; fileReader_/fileWriter_ encoding params → string; encryptString/decryptString plain/cipher text → string. headerTemplate indexes via Record<number, { fields: (string|number)[][] }>; encodeHeader_ parameter typed; decodeHeaderString.cipherTextstring; decodeHeaderBytes_.outputRecord<string, string|number>. itemIsEncrypted.item typed { encryption_applied?, encryption_cipher_text?, type_? }. The 13 that stay (with reasons) are fsDriver_, 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.execute reason updated. ExportModule.onProcessItem/onProcessResource reasons updated. KeymapItem.userData/ImportContext.options/Script.onStart typed unknown/Record<string, unknown>. MenuItem.commandArgs: any[]unknown[]. FormValue.value/DialogResult.formDataunknown/Record<string, unknown>. SettingItem.optionsRecord<string|number, string>. ContentScriptModuleLoadedEvent.userDataunknown. 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.tsx casts formData from SerializableData to Record<string, unknown> at the call site.

Checkpoint 23 (2026-05-14): 477 → 433 (44 removed).

  • reducer.ts — 33 removed, 14 left. Introduced AdditionalReducer (kept as a 3-any interface — reducers from plugin/share services are immer-based with Draft mutation and per-sub-state shapes) and SearchEntry for State.searches. StateLastSelectedNotesIds.{Folder,Tag,Search}: anyRecord<string, string[]>; StateDecryptionWorker.decryptedItemCountsRecord<string, number>; State.masterKeys: any[]MasterKeyEntity[]; pluginsLegacy/syncReport/screens typed. derivedStateCache_Record<string, unknown>; cacheEnabledOutput made generic <T>. Selectors and selectArrayShallow typed. StateUtils.{selectArrayShallow,notesOrder,foldersOrder,lastSelectedNoteIds} typed; arrayHasEncryptedItems{ encryption_applied? }[]; removeAdjacentDuplicates made generic. (windowDraft as any)[k] patterns replaced with (windowDraft as unknown as Record<string, unknown>)[k]. Helpers updateOneItem, handleHistory, getContextFromHistory, removeItemFromArray, the top-level reducer body keep any with descriptive reasons matching the redux-action-union discrimination pattern. Side fixes: PLUGINLEGACY_DIALOG_SET and DECRYPTION_WORKER_SET reducer cases use typed intermediates.
  • models/BaseItem.ts — 11 removed, 20 left. ItemsThatNeedDecryptionResult.items: any[]BaseItemEntity[]; isSystemPath and pathToId use local const/split chains instead of mutating an any variable; loadItemByField.value: anystring|number|boolean; syncItemClassNames/syncItemTypes map callbacks typed; encrypt error and reducedItem casts now go through structural types; decrypt, serialize, unserialize, serialize_format/unserialize_format kept any with descriptive reasons for their unavoidably heterogeneous parameters. updateSyncTimeQueries/saveSyncTime kept any because tests pass loose objects with id as 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: anyJoplinDatabase. defaultValues.output: anyRecord<string, unknown>; applySqlOptions.params/all.params typed as primitive arrays; allIds.rows.map typed inline. count.options/loadByField.fieldValue/loadByFields.fields/loadByTitle.fieldValue/fieldType.defaultValue/releaseSaveMutex.release/saveMutex.modelOrId all typed concretely. saveQuery.temp/filtered: anyRecord<string, unknown>. userSideValidation.o typed { id?, user_updated_time?, user_created_time? }. count.then callback typed. Several stay any with 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), and dispatch: Function for variance.

Checkpoint 25 (2026-05-14): 411 → 361 (50 removed).

  • components/shared/note-screen-shared.ts — 21 removed, 2 left. Introduced AttachedResource/AttachedResources/SaveNoteOptions/AttachFileAsset/ResourceHandler types. BaseState.noteResources: anyAttachedResources. Shared interface methods all typed (saveOneProperty/noteComponentchange/installResourceHandling/uninstallResourceHandling/attachedResources/toggleCheckboxRange). saveNoteButton_press.options: anySaveNoteOptions. newState: anyPartial<BaseState>. `resourceCache: anyAttachedResources. 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. Introduced ProxySettings interface; proxySettings.any and setupProxySettings.options.any typed. ShimInitOptions any fields kept with descriptive reasons (sharp/keytar/React/electronBridge/nodeSqlite are external module types). detectAndSetLocale.Setting: anytypeof Setting. saveOptions: any → typed structurally. imageOptions: any → typed. cleanUpOnError/file/request.on('error') typed Error. requestOptions kept any (node http/https request options + agent). sites call site typed via NodeJS.CallSite[]. makeResponse.response typed structurally.
  • import-enex.ts — 17 removed, 3 left. sourceStream/destStream.on('error') typed Error. removeUndefinedProperties made generic <T>. saveNoteResources.toSave: any cast tightened via Partial<Pick<...>> for the delete keys. Node.attributes: Record<string, any>Record<string, string>. saveNoteToStorage cast through as ExtractedNote. handleSaxStreamEvent typed (...args: any[])=> void with reason (sax events heterogeneous). noteAttributes/noteResourceAttributes typed Record<string, string>. createErrorWithNoteTitle typed. Stream and saxStream on('error') typed Error. noteResource[n] indexing uses a typed cast through Record<string, string>. is_todo cast to 0|1. Latitude/longitude/altitude need as unknown as number casts 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: anystring. dispatch: Function → action-shape dispatch. syncCalls_: any[]boolean[]. scheduleSyncTablesIID_ typed via ReturnType<typeof shim.setTimeout>. setDb.db: anyJoplinDatabase. fieldNamesFromOffsets_.offsets: any[]number[]. hitsThisRow/docsWithHits typed Uint32Array. processBasicSearchResults_/processResults_ typed ProcessResultsRow[] + ParsedQuery. queryTermToRegex.term: anystring. basicSearch.searchOptions: any typed structurally; determineSearchType_.preferredSearchTypeSearchType; allTerms: any[]Term[]. Side: added fuzziness?: number to ProcessResultsRow (was being assigned at runtime but not declared).
  • import-enex-md-gen.ts — 16 removed, 5 left. Section.lines: any[] kept any with reason (mixed strings/Section/Hr objects). ParserState.{anchorAttributes,spanAttributes}: any[]Record<string, string>[]. collapseWhiteSpaceAndAppend.state: anyParserState. Introduced SaxContext type alias; cssValue/isInvisibleBlock/isHighlight/isCodeBlock/displaySaxWarning typed via SaxContext and { style?: string }. attributeToLowerCase.node typed structurally. isSpanWithStyle/isSpanStyleBold/isSpanStyleItalic.attributes typed. saxStream.on('error') typed Error; saxStream.on('opentag') node: any{ name; attributes? }. captionLines: any[] lets inference do the work. enexXmlToMdArray.stream, renderLine/renderLines.lines, and currentCells keep any with 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: anyReturnType<typeof shim.setInterval>. lockFileToObject.file: any typed structurally with optional path/updated_time.
  • services/synchronizer/utils/types.ts — 1 removed, 1 left with updated reason. LogSyncOperationFunction.local: any typed structurally; ApiCallFunction keeps any[]/any (dispatches by name across drivers).
  • services/style/themeToCss.ts — 2 removed, 0 left. isColor.v typed unknown with type predicate; theme indexing via Record<string, unknown> cast.
  • services/style/cssToTheme.ts — 2 removed, 0 left. declarations/output typed; final return cast as unknown as Theme.
  • services/profileConfig/mergeGlobalAndLocalSettings.ts — 2 removed, 0 left. rootSettings/subProfileSettings/output all Record<string, unknown>.
  • utils/attachedResources.ts — 2 removed, 0 left. resourceCache_/output: anyAttachedResources.
  • services/noteList/renderTemplate.ts — 2 removed, 0 left. Cell.value: anyunknown; valueToString.value: anyunknown (using String(value)).
  • services/plugins/api/JoplinWindow.ts — 2 removed, 0 left. Introduced local DispatchStore type for store_.
  • services/plugins/api/Global.ts — 2 removed, 0 left. implementation: anyBasePlatformImplementation; store: anyStore<any> with descriptive reason; process: anyNodeJS.Process.
  • services/plugins/api/Joplin.ts — 1 removed, 1 left with updated reason. store: anyStore<any> with reason. require.return: any kept with plugin-API reason.
  • services/plugins/api/JoplinCommands.ts — 2 removed (reasons updated).
  • services/plugins/utils/loadContentScripts.ts — 1 removed, 1 left. loadedModule as any cast → typed extension. ExtraContentScript.module: any kept with descriptive reason.
  • services/ResourceService.ts — 2 removed, 0 left. maintenanceTimer1_/maintenanceTimer2_: anyReturnType<typeof shim.setTimeout/setInterval>.
  • services/KeymapService.ts — 2 removed, 0 left. modifiersRegExp: anyRegExp; domToElectronAccelerator.event: any typed structurally with the 5 fields actually read.
  • services/interop/InteropService_Importer_Base.ts — 2 removed, 0 left. setMetadata.md: anyPartial<ImportMetadata> with cast; init.options: anyImportOptions.
  • services/interop/InteropService_Importer_Custom.ts — 2 removed, 0 left. options: anyRecord<string, unknown> on CustomImporter.onExec.context.options and processedOptions.
  • services/interop/InteropService_Exporter_Raw.ts — 2 removed, 0 left. processItem.item/processResource.resource typed via BaseItemEntity/ResourceEntity.
  • utils/ipc/utils/mergeCallbacksAndSerializable.test.ts — 1 removed, 1 left. data: any typed inline.
  • utils/ipc/RemoteMessenger.test.ts — 2 removed, 0 left. transfer.o: any made generic <T>; testObjects: any[]Record<string, unknown>[].
  • Side fix: services/interop/InteropService_Importer_Md.test.ts uses ImportModuleOutputFormat enum 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.options typed; (n as any)[field] and (o as any)[field] patterns → Record<string, unknown> casts; beforeChangeItems: anyRecord<string, string | null>; updateNoteOrder_.ordernumber; handleTitleNaturalSorting.options typed.
  • models/BaseItem.ts — 4 removed, 16 left. displayTitle.item typed structurally; items.map((item: any)) typed via modelSelectAll<{ id: string }>; markdownTag.itemOrId/isMarkdownTag.md typed structurally; save.o reason updated.
  • BaseModel.ts — 3 removed, 16 left. isNew.object/options typed; filterArray/filter reasons updated; static enum loop uses Record<string, ModelType> cast.
  • models/Folder.ts — 1 removed, 0 left. report: anyRecord<string, number>.
  • services/plugins/Plugin.ts — 1 removed, 0 left. emit.event: anyunknown.
  • services/plugins/api/JoplinPlugins.ts — 1 removed, 0 left. script.onStart.catch.error: any typed structurally.
  • services/commands/MenuUtils.ts — 1 removed, 0 left. commandToStatefulMenuItem.commandTarget: anyunknown.
  • services/interop/InteropService_Exporter_Md_frontmatter.ts — 0 removed (reason updated).
  • import-enex.ts — 1 removed, 2 left. saxStream cdata callback typed string.
  • testing/test-utils-synchronizer.ts — 0 removed (kept with reason matching the heterogeneous test fixtures).
  • services/CommandService.test.ts — 1 removed, 1 left. createCommand.options kept any with reason (test fixtures), execute mock kept with reason.
  • services/synchronizer/Synchronizer.conflicts.test.ts — 2 removed, 0 left. dynamic field iteration uses Record<string, unknown> casts.
  • services/rest/Api.test.ts — 2 removed, 0 left. response: anyNoteEntity; 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:

  1. Per-app polymorphism: shim.ts and shim-init-node.ts cross-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.
  2. 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.
  3. 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.
  4. 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.
  5. Reducer action unions: reducer.ts matches dynamically on action.type across all redux actions in the app; the action union diverges across cli/desktop/mobile so a strict union here would not compose.
  6. Test fixtures: a handful of tests pass loose { id: 1, type_: ... } objects with id as number, mocked stores without dispatch, 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.