any Cleanup Progress - details
packages/pdf-viewer
Session date: 2026-05-11
Files processed:
messageService.ts— 1 removed, 0 left. Replaceddata?: anywithdata?: Record<string, unknown>.Page.tsx— 1 removed, 0 left. Replacedlet style: anywithlet style: CSSProperties(imported fromreact).PdfDocument.ts— 3 removed, 0 left. ImportedPDFDocumentProxyandPDFPageProxyfrompdfjs-dist; typeddoc,pages(asRecord<number, PDFPageProxy>) and the localpdfDocument.
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/pdf-viewer/ clean.
packages/react-native-saf-x
Session date: 2026-05-11
Files processed:
src/index.ts— 1 removed, 0 left. Replaced{} as anywith{} as SafxInterface(the interface declared in the same file).
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/react-native-saf-x/ clean.
packages/default-plugins
Session date: 2026-05-11
Files processed:
build.ts— 4 removed, 0 left. ImportedArgvandArgumentsCamelCasefromyargs; typed builder callbacks as(yargs: Argv) => ...and handler args asArgumentsCamelCase<{ outputDir: string }>/ArgumentsCamelCase<{ plugin: string }>.
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/default-plugins/ clean.
packages/editor
Session date: 2026-05-11
Files processed:
CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts— 2 removed, 6 left.- Removed:
isPositiontype guard now usesPartial<DocumentPosition>instead ofany;removeOverlayoverlay param usesOverlayType<unknown>instead ofOverlayType<any>. - Left:
OptionUpdateCallbacknewVal/oldVal: any(the source isvalue: any, which is API-driven; narrowing in callbacks would be a logic change);addOverlayreturn type (theanystructurally deceives the base-class signatureSearchQuery | undefinedto allow returning{ clear: () => void }from the decorator branch — fixing it is a class-hierarchy refactor, not a typing tweak);commands as anycast (same kind of structural deception of the base class); and the 4 entries already tagged "CodeMirror 5 API requires any" / "Must match base class signature". Re-checked after the rule was clarified to allow small new type definitions — none of these are amenable to that.
- Removed:
CodeMirror/pluginApi/PluginLoader.ts— 4 removed, 1 left.- Removed: introduced
PluginLoaderWindowtype alias (Window & { __pluginLoaderScriptLoadCallbacks: Record<number, OnScriptLoadCallback>; __pluginLoaderRequireFunctions: Record<number, typeof codeMirrorRequire> }); replaced four(window as any).__pluginLoader…casts with(window as unknown as PluginLoaderWindow).…. - Left:
OnScriptLoadCallbackexports: any(already tagged "Plugin exports have dynamic structure").
- Removed: introduced
Files skipped entirely (only non-"Old code" tags inside):
types.ts—execCommand/varying argument types.CodeMirror/editorCommands/editorCommands.ts—EditorCommandFunctionvarying argument types.CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.test.ts— dynamic-extension test casts.
Verification: yarn tsc --noEmit clean, yarn linter-ci packages/editor/ clean.
packages/utils
Session date: 2026-05-11
Files processed:
dom.ts— 1 removed, 0 left.findParentElementByClassNameparameter typedElement | null(broad enough to acceptEventTarget & Elementfrom callers in app-desktop).splitCommandString.ts— 1 removed, 0 left. Introduced localSplitCommandStringOptions { handleEscape?: boolean }interface.cli.ts— 0 removed, 1 left. TriedInterfacefromreadline/promises, but@types/nodein this repo does not declare thereadline/promisessubmodule. Updated reason on the disable comment to reflect that.execCommand.ts— 1 removed, 1 left.envtyped asRecord<string, string | undefined>. The other entry is already tagged with a Workaround reason (Expo/NodeJs.ProcessEnv conflict).net.ts— 1 removed, 0 left. Introduced localFetchWithRetryOptions extends RequestInitinterface withretry,callback,pausefields.object.ts— 0 removed, 2 left.objectValueFromPathdoes successive indexing (result = result[e]) which requiresany; tightened toRecord<string, unknown>failed because intermediate values areunknown.checkObjectHasPropertiesis called withNoteEntity/FolderEntity/ItemSlice(interfaces without index signatures) — tightening forces every caller to widen. Updated reasons to explain why.html.ts— 1 removed, 0 left.attributesHtml(attr: Record<string, string>)to match the local-only usage; renderer package has its ownattributesHtmlalready typed the same way.Logger.ts— 17 removed, 1 left.- Removed:
TargetOptions.consoletypedConsole;Logger.createwrapper argsunknown[];addTargetfield copy uses pairedRecord<string, unknown>casts;objectToStringouter paramunknown, inner Error branch uses a typed intersection cast;objectsToString,error/warn/info/debugrest argsunknown[];items: unknown[]; global-logger fallback typed as aLoggercast;consoleObj[fn]indexed throughRecord<string, (...args: unknown[]) => void>cast. - Left:
TargetOptions.database— tightening leaks throughlastEntries()to all downstream consumers (e.g.app-mobile/exportDebugReport.ts) that read.timestamp/.level/.messageviaany. Refactoring those is out of scope. Updated reason on the disable comment.
- Removed:
env.ts— 2 removed, 0 left.(error as Error).message = …;key_value = … as RegExpMatchArray(and the inner nested.match()typed the same way to preserve the original implicit non-null assumption).
Verification: yarn tsc --noEmit clean for the utils package; root yarn tsc --noEmit (all workspaces) clean — initial attempt broke 4 downstream files in app-desktop/app-mobile/server/tools, which forced reverts on checkObjectHasProperties and LoggerDatabase and a widening of findParentElementByClassName. yarn linter-ci packages/utils/ clean.
packages/renderer
Session date: 2026-05-11
Shared work: added a new RendererTheme interface in types.ts (a structural superset of the application theme that the renderer reads — cacheKey, appearance, colors, noteViewerFontSize, bodyPaddingTop/Bottom, buttonStyle, etc.). All fields are optional because callers pass either ThemeStyle from @joplin/lib, the renderer's defaultNoteStyle merge, or bare {} (tests). This made it possible to replace theme: any across the package without touching call sites in app-desktop, app-mobile, server, lib/commands/renderMarkup, etc. Also exported ResourceEntity from types.ts so Link.resource could be typed.
Files processed:
types.ts— 7 removed, 1 left.- Removed:
theme?,plugins?(typed asRecord<string, Record<string, unknown>>so it can be spread),MarkupRenderer.render,MarkupRenderer.allAssets,MarkupToHtmlConverter.render(also tightenedoptions: any→RenderOptions),MarkupToHtmlConverter.allAssets. - Left:
FsDriver.cacheCssToFile— return is used loosely as aRenderResultPluginAssetpush target while the actual returned object only has{ path, mime }; tightening forces logic changes. Updated reason on the comment.
- Removed:
HtmlToHtml.ts— 2 removed, 0 left. Boththeme: any→RendererTheme. Also madeOptions.ResourceModeloptional (constructor already handlesnull).MarkupToHtml.ts— 4 removed, 0 left.rawMarkdownIt_typed asMarkdownIt(added a type-onlyimport type * as MarkdownItType from 'markdown-it'). Theas anyon theRendererClassconstruction went away by branching to explicitnew MdToHtml(this.options_)/new HtmlToHtml(this.options_)calls.theme: anyonrenderandallAssets→RendererTheme.MdToHtml.ts— 19 removed, 4 left.- Removed:
RendererRule.install(typed viaPluginContext/RuleOptions/unknown),RendererRule.assets((theme: RendererTheme) => PluginAsset[]),RendererRule.plugin(MarkdownIt.PluginWithOptions);RendererPlugin.moduleandoptions(typed plugin +Record<string, unknown>);ExtraRendererRule.module(Omit<RendererRule, 'assetPath' | 'pluginId' | 'assetPathIsAbsolute'>);Options.pluginOptions(Record<string, { enabled?: boolean } & Record<string, unknown>>);Link.resource(ResourceEntity | null); the fourPluginContextanyfields (Record<string, string>for css,Record<string, PluginAsset[]>for pluginAssets,InMemoryCachefor cache,Record<string, Record<string, unknown>>for userData);RuleOptions.theme(RendererTheme);pluginOptions_(NonNullable<Options['pluginOptions']>);loadExtraRendererRule.module(matches newExtraRendererRule['module']);allProcessedAssets.theme,allProcessedAssets.assets(PluginAssets);allAssets.themeand innerassets(PluginAssets);render.theme(RendererTheme);_attrs: anyonhighlight()→string. - Left:
outputAssetsToExternalAssets_(output: any)— the function mutates withdelete output.cssStrings, whichRenderResultdoes not permit; updated reason on the comment.render'stheme: any = nullwas tightened totheme: RendererTheme = null(note: keepingnulldefault required RendererTheme to allownullvia callers passing it through, handled because all fields are optional).highlight()returnsanybecause the function returns either astringor{ wrapCode, html }depending on whetherrules.fenceis set — a non-standard markdown-it return.loadPlugin(plugin: any, options: any)— plugin may be a function or an ES-module wrapper with.default, and the function reassigns it before callingmarkdownIt.use(plugin, options); tightening forces logic restructuring. Updated reasons.
- Removed:
noteStyle.ts— 1 removed, 0 left.theme: any→RendererTheme(all fields optional, sotheme = theme ? theme : {}continues to type-check).headerAnchor.ts— 2 removed, 0 left. Added type-only imports forMarkdownItandmarkdown-it/lib/rules_core/state_core;markdownIt: any→MarkdownIt,state: any→StateCore.InMemoryCache.ts— 0 removed, 3 left. Generic cache: values are heterogeneous across calls (different keys store different shapes);unknownwould force narrowing changes at every caller. Updated reason on one comment that was tagged "Old code" to match the existing "Generic cache" reason.MdToHtml/setupLinkify.ts— 2 removed, 0 left. Added type-only imports forMarkdownItandlinkify-it;markdownIt: any→MarkdownIt,self: any→LinkifyIt.LinkifyIt.MdToHtml/linkReplacement.ts— 1 removed, 0 left.LinkReplacementResult.resource: any→ResourceEntity | null.MdToHtml/renderMedia.ts— 1 removed, 0 left.Options.theme: any→RendererTheme.
Verification at checkpoint (after top-level files, before processing MdToHtml/rules/*): package yarn tsc --noEmit clean; root yarn tsc --noEmit clean.
Shared work for the rules subdirectory: across most rules the markdownIt: any → MarkdownIt, state: any → StateCore / StateInline / StateBlock, tokens: any[] → Token[], Token: any → typeof import('markdown-it/lib/token'), and (tokens, idx, options, env, self) rule signatures pick up Renderer.RenderRule (or its component types). All these are type-only imports from markdown-it's @types package, so they have no runtime cost. Two RuleOptions extensions: globalSettings, settingValue (set per-rule by MdToHtml.render()), and mapsToLine (used only by source_map rule).
One unrelated typing fix: utils.getAttr(attrs: string[], ...) → (attrs: [string, string][], ...) — the function indexes attrs[i][0] / attrs[i][1], so the original string[] signature was wrong and prevented typing the tokens[idx].attrs calls in image.ts correctly.
Files processed:
MdToHtml/rules/abc.ts— 1 removed, 1 left.ruleOptions: any→RuleOptions(the existing comment said "we still don't have a type for ruleOptions"; we do now). Left:(self.renderToken as any)(tokens, idx, options, env, self)— extra args beyond the typed signature; reason already documented.MdToHtml/rules/code_inline.ts— 3 removed, 0 left. TypeddefaultRender: Renderer.RenderRule; outermarkdownIt: any→MarkdownIt; rule signature gets proper types.MdToHtml/rules/externalEmbed.ts— 0 removed, 2 left. Both(self.renderToken as any)(...)casts passenv, selfwhichrenderToken's typed signature does not declare. Updated reasons.MdToHtml/rules/checkbox.ts— 5 removed, 0 left.theme: any→RendererTheme;Token: any→typeof import('markdown-it/lib/token');sourceToken: any→Token;markdownIt: any→MarkdownIt;state: any→StateCore.MdToHtml/rules/fence.ts— 2 removed, 1 left. Typed the rule signature withToken[]/MarkdownIt.Options/Renderer. Left:options: anyon the fence renderer —options.highlighthere returns either a string or{ wrapCode, html }; the typed signature can't express that disjunction without restructuringMdToHtml.render.tmpToken as Tokencast added because the rule constructs a partialToken({ attrs }) just to callslf.renderAttrs, which only readsattrs.MdToHtml/rules/image.ts— 2 removed, 0 left.markdownIt: any→MarkdownIt; rule signature gets typed.MdToHtml/rules/fountain.ts— 4 removed, 0 left.theme: any→RendererTheme(the existing comment said "Theme is defined in @joplin/lib and we don't import it here" — but now we have a localRendererTheme);markdownIt: any→MarkdownIt; rule signatures typed.MdToHtml/rules/highlight_keywords.ts— 4 removed, 0 left.Token: any→typeof import('markdown-it/lib/token');markdownIt: any→MarkdownIt;state: any→StateCore.MdToHtml/rules/katex.ts— 9 removed, 0 left. IntroducedKatexMacroToken,KatexMacro,KatexOptionslocal interfaces (matching the macro shape documented in the file's own comments).stringifyKatexOptions,renderToStringWithCache, and the innertoSerializenow use these types.(t: any) => t.texttyped via theKatexMacroToken.textfield.state: any→StateInline/StateBlock;markdownIt: any→MarkdownIt; the two renderer functions typed viaToken[]. A singleas KatexOptions['macros']cast on the macros pulled fromoptions.context.userData.__katex.macros, sinceuserDatais typedRecord<string, Record<string, unknown>>and the renderer doesn't know the per-plugin shape there.MdToHtml/rules/html_image.ts— 6 removed, 0 left. Both default render fallbacks typed asRenderer.RenderRule;handleImageTagsnow returnsRenderer.RenderRule; outermarkdownIt: any→MarkdownIt; innercontent.replacecallback usesstringinstead ofany.MdToHtml/rules/link_close.ts— 3 removed, 0 left.defaultRender: Renderer.RenderRule;markdownIt: any→MarkdownIt; rule signature typed.MdToHtml/rules/mermaid.ts— 3 removed, 1 left.theme: any→RendererTheme;markdownIt: any→MarkdownIt; rule signature typed. Left: the(self.renderToken as any)(tokens, idx, options, env, self)cast in the fence-rule fallback — same pattern asabc.ts/externalEmbed.ts, passes extra args beyond the typedrenderTokensignature.MdToHtml/rules/sanitize_html.ts— 3 removed, 0 left.markdownIt: any→MarkdownIt;state: any→StateCore;tokens: any[]→Token[].MdToHtml/rules/link_open.ts— 2 removed, 0 left.markdownIt: any→MarkdownIt; rule signature typed.MdToHtml/rules/source_map.ts— 2 removed, 0 left.params: any→RuleOptions(addedmapsToLine?: booleantoRuleOptions); rule signature typed.
Verification: package yarn tsc --noEmit clean, yarn linter-ci packages/renderer/ clean, root yarn tsc --noEmit clean.
Summary: 99 → 12 disable comments. The 12 remaining are all either documented skip cases (cache values, as any to pass extra args to typed renderToken, output: any that mutates delete output.cssStrings, cacheCssToFile return used loosely, loadPlugin accepting both functions and { default } wrappers, the non-standard highlight() return shape, generic in-memory cache) or genuinely cannot be replaced without code-logic changes that are out of scope for this PR.
packages/tools
Session date: 2026-05-11
Note: the starting count of 49 included 3 disables inside packages/tools/node_modules/ (docusaurus and one in node_modules/@docusaurus/utils) which are vendored copies of third-party code and not in scope. In-scope was 46; after this session, 1 in-scope and 3 vendored remain (total 4).
Files processed:
postPreReleasesToForum.ts— 1 removed, 0 left.processedReleases: Record<string, any>→Record<string, boolean>(the stored value is alwaystrue).generate-images.ts— 1 removed, 0 left.output: any[]→(string | number | undefined)[](matches the heterogeneous values pushed fromOperation).generate-database-types.ts— 5 removed, 0 left. ImportedTableandColumnfrom@rmp135/sql-ts/dist/Typings;createRuntimeObject/generateListRenderDependencyType/the two.map()callbacks all typed via those. The column-push usesas Columnbecause adding the missingColumnDefinitionfields explicitly may changesql-ts.fromObject()output.update-readme-contributors.ts— 1 removed, 0 left. Therequestcallback typed(error: Error | null, response: { statusCode: number }, data: Contributor[]).release-clipper.ts— 1 removed, 0 left. Introduced localManifest = Record<string, unknown> & { background?, browser_specific_settings? };removeManifestKeys(manifest: Manifest): Manifest.update-readme-download.ts— 1 removed, 0 left.main(argv: any)→main(argv: string[])(called asmain(process.argv)).setupNewRelease.ts— 1 removed, 0 left. yargs.argvcast to{ _: string[]; updateVersion?: string; updateDependenciesVersion?: string }(rather thanyargs.Arguments<T>, becauseArguments._isArray<string | number>andparse-numbers: falseguarantees strings at runtime — using the library type would require a_[0] as stringcast at every call site).tool-utils.ts— 11 removed, 0 left.saveGitHubUsernameCache(cache: any)→Record<string, string>;execCommandoptions typed{ cwd?; env?; maxBuffer? }and its callback(error: (Error & {signal?: string}) | null, stdout: string, stderr: string);execCommandWithPipeserror: Error,code: number | null;setPackagePrivateField(value: any)→boolean;downloadFile'shttps.getcallbackresponse: import('http').IncomingMessage,error: Error;fileSha256streamdata: Bufferanderror: Error;fileExistsstat callbackerror: NodeJS.ErrnoException | null;gitHubLatestRelease/gitHubLatestRelease_KeepInCaseMicrosoftBreaksTheApiAgainresponse: anydeleted (node-fetch's inferredResponseworks fine);githubRelease(options: any)→{ isDraft?: boolean; isPreRelease?: boolean }.build-release-stats.ts— 0 removed, 1 left.createMarkdownTableis typed(headers, rows: MarkdownTableRow[])whereMarkdownTableRow = Record<string, string>.Releaserows contain numeric counts (windows_count, etc.), so the castrows as any[]masks a real type mismatch. Tightening would require wideningcreateMarkdownTablein@joplin/lib(cross-package change). Updated reason.licenses/licenseChecker.ts— 1 removed, 0 left.enforceString(line: any)→string | string[] | undefined | null(matches theArray.isArray(line) ? line.join(', ') : ...branches).website/updateNews.ts— 1 removed, 0 left. Introduced localRssFeedIteminterface (rssis untyped);feedItems: RssFeedItem[].website/processDocs.ts— 3 removed, 0 left.currentLinkAttrs?: any→[string, string][] | null; importedTokenfrommarkdown-it/lib/tokensoprocessToken(token: Token, …);onopentagattrs: Record<string, any>→Record<string, string>(matcheshtmlentities(attrs[n])usage and the localattributesHtmlsignature).website/build.ts— 1 removed, 0 left.scriptsToImport: any[]→{ id: string; sourcePath: string; md5: string; filename: string }[](the entries are all commented out, but the shape is implied by the loop that follows).website/utils/applyTranslations.ts— 1 removed, 0 left.onopentagattrs: any→Record<string, string>.website/utils/convertLinksToLocale.test.ts— 1 removed, 0 left.[string, any, string][]→[string, Partial<Locale>, string][]with alocale as Localecast at the call site (the test cases only supplypathPrefix, not the fullLocaleshape).website/utils/frontMatter.ts— 3 removed, 0 left.yaml.load(...)cast directly toFrontMatter;formatFrontMatterValue(value: any)→FrontMatter[keyof FrontMatter]; the(header as any)[key] = …assignment refactored to a separateRecord<string, string>output map.website/utils/frontMatter.test.ts— 1 removed, 0 left.testCases: any[][]→[string, FrontMatter, string, string][].website/utils/render.ts— 1 removed, 0 left.state: any→StateCore(imported frommarkdown-it/lib/rules_core/state_core).utils/translation.ts— 1 removed, 0 left. Introduced localGettextTranslationandGettextParsedinterfaces capturing only the fields read (gettext-parserisrequire-d and has no types).serializeTranslation's parameter inbuild-translation.tswas incorrectly typed asstring— corrected toParameters<typeof parseTranslations>[0].utils/discourse.ts— 5 removed, 0 left. IntroducedDiscourseApiError extends Error { apiObject; status };new Error(...) as DiscourseApiErrorthen settingerror.apiObject/error.statusdirectly.response.json() as anydeleted — node-fetch's inferred return works fine for the downstream.error.status === 404reads.createTopic/createPost/updatePostbody params →Record<string, string | number>(matchesexecApi).
Verification: package yarn tsc --noEmit clean, yarn linter-ci packages/tools/ clean, root yarn tsc --noEmit clean.
Summary: 49 → 4 disable comments; in-scope 46 → 1 (3 are inside node_modules/ vendored copies, not touched). The single in-scope remaining one is build-release-stats.ts, where fixing would require widening createMarkdownTable's signature in @joplin/lib.
packages/plugin-repo-cli
Session date: 2026-05-11
Shared work: imported PluginManifest from @joplin/lib/services/plugins/utils/types (plugin-repo-cli already depends on @joplin/lib). Most manifest: any and manifests: any parameters became PluginManifest and Record<string, PluginManifest>. The *.test.ts files in this package frequently use partial manifest fixtures, so a few as unknown as PluginManifest casts at call sites were needed to keep tests typing without bulking up every fixture with the full manifest_version/name/app_min_version set. Two helper functions were widened structurally (rather than via casts) where they only read a subset of fields: gitCompareUrl now takes Pick<PluginManifest, 'repository_url' | '_publish_commit'>, and getObsoleteManifests became a generic <T extends { _obsolete?: boolean }> so the existing-test fixtures still type-check.
Files processed:
index.ts— 13 removed, 0 left.extractPluginFilesFromPackagetypedexistingManifests: PluginManifests, returnPromise<PluginManifest>;files.find((f: any) => …)→(f: string)(sincefs.readdirreturnsstring[]);commitMessageparameters typed (manifest: PluginManifest | null,previousManifest: PluginManifest | null,error: Error | null);readManifests/writeManifestsreturn/takePluginManifests; the fourlet X: any = {}locals inprocessNpmPackagetyped; the yargs handlers refactored —setSelectedCommandand the three command handlers now share aCommandArgstype matching the{ pluginRepoDir, dryRun }shape bothcommandBuildandcommandUpdateReleaseexpect (the previousselectedCommandArgs = ''declaration also implicitly drifted from string → object);commands: Record<string, Function>replaced withRecord<string, (args: CommandArgs)=> Promise<void>>. The twoawait readJsonFile(...)callers in this file pass an explicit type parameter (readJsonFile<PluginManifest>(...),readJsonFile<{ version: string }>(...)) sincereadJsonFileis now generic.commands/updateRelease.ts— 3 removed, 0 left.(error: Error, assets: any)→assets: unknown(return is not consumed by callers);(resolve: Function, reject: Function)ban-types disable replaced with explicit(assets: unknown)=> void/(error: Error)=> void; introduced localPluginVersionStatsandPluginStatstypes and used them forcreateStats/saveStats.lib/checkIfPluginCanBeAdded.ts— 2 removed, 0 left.caseInsensitiveFindManifest/default-export both typed viaPluginManifestsandPluginManifest.lib/overrideUtils.ts— 3 removed, 0 left.applyManifestOverrides(manifests: PluginManifests, …); the innermanifest[propName] = propValuewrite usesas PluginManifest & Record<string, unknown>because we mutate via afor...ofoverObject.entries(override)keys.getObsoleteManifestsmade generic over<T extends { _obsolete?: boolean }>so the existing test (which passes manifest-shaped fixtures rather thanManifestOverride-shaped ones) types correctly without rewriting its data.lib/errorsHaveChanged.test.ts— 1 removed, 0 left.testCases: any[][]→[ImportErrors, ImportErrors, boolean][]; dropped theas anycast in the destructuring loop.lib/validateUntrustedManifest.ts— 1 removed, 0 left. Parameters typedmanifest: PluginManifest,existingManifests: Record<string, PluginManifest>.lib/updateReadme.test.ts— 1 removed, 0 left.manifests: any→Record<string, PluginManifest>.lib/updateReadme.ts— 2 removed, 0 left. Parametermanifests: Record<string, PluginManifest>; therows.push(manifests[pluginId])line uses anas unknown as MarkdownTableRowcast (the existing code stores PluginManifest objects inside aMarkdownTableRow[]andcreateMarkdownTableis typed forRecord<string, string>rows — the same impedance mismatch documented onbuild-release-stats.tslast session); therows.sort((a: any, b: any) => …)callback's args type-infer fromMarkdownTableRowdirectly.lib/utils.ts— 2 removed, 0 left.readJsonFilemade generic<T = unknown>(manifestPath, defaultValue: T = null): Promise<T>;isJoplinPluginPackage(pack: any)→{ keywords?: string[]; name: string }(matches the two fields it reads).lib/overrideUtils.test.ts— 2 removed, 0 left. First test'smanifestOverrides: any→Record<string, { _obsolete?: boolean; description?: string; [key: string]: unknown }>(matches the heterogeneous fixture). Second test'smanifests: any→ inferred from the literal.lib/gitCompareUrl.ts— 1 removed, 0 left. Widened toPick<PluginManifest, 'repository_url' | '_publish_commit'>(matches the fields the function actually reads, so the test fixtures don't need to spell out the wholePluginManifest).lib/checkIfPluginCanBeAdded.test.ts— needed touching even though it had no disable comments: the test passes partial fixtures, so call sites cast viaas unknown as Parameters<typeof checkIfPluginCanBeAdded>[0/1]. Same pattern inlib/validateUntrustedManifest.test.ts(as unknown as PluginManifest) andlib/gitCompareUrl.test.ts(where the tuple is typed[GitCompareManifest, GitCompareManifest | null, string | null]).
Verification: package yarn tsc --noEmit clean, yarn linter-ci packages/plugin-repo-cli/ clean, root yarn tsc --noEmit clean, yarn test (in plugin-repo-cli) clean — all 13 tests pass.
Summary: 33 → 0 disable comments. Every one was tagged Old code before rule was applied and could be replaced with PluginManifest, ImportErrors, structural picks, or generics.
packages/app-mobile
Session date: 2026-05-12
Note: starting baseline was 131 comments across 37 files. The grep also matched build artifacts under packages/app-mobile (compiled .js/.bundle.js.map) which are gitignored; the source counts (.ts/.tsx only) match the 131/37 baseline exactly.
Files processed (partial — checkpoint):
utils/debounce.tsx— 2 removed, 0 left. Replaced(...args: any[])with generic<Args extends unknown[]>(...args: Args)=> voidso callers likeNote.tsx's(event: EditorChangeEvent)=> voidstill type-check.utils/getVersionInfoText.ts— 1 removed, 0 left. Replaced(global as any).HermesInternalwith(global as { HermesInternal?: unknown }).HermesInternal.components/screens/ConfigScreen/types.ts— 1 removed, 0 left.UpdateSettingValueCallbackvalue paramany→unknown(callers don't read it as typed values).components/base-screen.ts— 1 removed, 0 left.Record<number, any>→Record<number, ReturnType<typeof StyleSheet.create>>.utils/database-driver-react-native.ts— 4 removed, 0 left.react-native-sqlite-storageships no.d.tsand no@types/...is installed, so introduced localSqliteDb/SqliteResultSetinterfaces covering the two methods used (executeSql, returning rows/insertId). The baseDatabaseDriverinterface usesSelectResult = any, so the narrower return type still satisfies it.utils/buildStartupTasks.ts— 1 removed, 0 left.resourceFetcher_downloadComplete(event: any)→event: { id: string; encrypted: boolean }(the shape emitted byResourceFetcher.eventEmitter_.emit('downloadComplete', ...)).index.web.ts— 1 removed, 0 left. Removed theRoot as anycast —Root extends React.ComponentandAppRegistry.registerComponent'sComponentProvideris() => React.ComponentType<any>, which() => Rootsatisfies.utils/fs-driver/fs-driver-rn.ts— 7 removed, 2 left (down from 9).- Removed:
appendFile/writeFilecontent paramany→string(base class usesstring);rnfsStatToStd_paramany→ localRnfsStatLikeunion ofStatResultT | ReadDirResItemT | DocumentFileDetailwithin-checks;readDirStatsoptionsany→ReadDirStatsOptions(withstats: RnfsStatLike[]);close(handle: any)→unknown(handle is ignored);readFileChunk(handle: any)→ typed inline{ path: string; offset: number; mode: string; stat: { size: number } | null };tarExtract/tarCreateoptionsany→Omit<Parameters<typeof tar*>[0], 'cwd'> & { cwd?: string }. - Left: the inner
output: any[]inreadDirStats— entries can be eitherDocumentFileDetail(SAF) or the normalizedStat-shaped object fromrnfsStatToStd_, the recursion helpers also accept this heterogeneous shape, so a single concrete type would force restructuring. Reason updated on the disable comment.
- Removed:
services/AlarmServiceDriver.android.ts— 2 removed, 0 left.@joplin/react-native-alarm-notificationships no types, so introduced localScheduledAlarm { id: string; data?: { joplinNotificationId?: number } }covering the two fields read.services/AlarmServiceDriver.ios.ts— 6 removed, 0 left. ImportedPushNotification,PushNotificationPermissions,ScheduleLocalNotificationDetailsfrom@react-native-community/push-notification-ios. Notes:requestPermissionsoptions{ alert: 1, badge: 1, sound: 1 }→ booleans (the typed interface declares them asboolean?; runtime accepts both, but the type is stricter).hasPermissionsreturn tightened toPromise<boolean>andperm.alert && perm.badge && perm.soundwrapped in!!()since the fields are optional booleans.scheduleNotification'siosNotificationkeepsid: stringand a finalas ScheduleLocalNotificationDetailscast because the library's typedScheduleLocalNotificationDetailsdeclares neitheridnoralertBody?— but the runtime requiresid(andalertBodymay be omitted), so the cast preserves existing behavior.root.tsx— 4 removed, 5 left.- Removed:
generalMiddlewareinnerscheduleRefreshFolders((action: any) => storeDispatch(action), ...)simplified toscheduleRefreshFolders(storeDispatch, ...);componentDidUpdate(prevProps: any)andUNSAFE_componentWillReceiveProps(newProps: any)typed asAppComponentProps(this also forced adding the missingsyncStarted: booleanfield thatmapStateToPropspopulates but the interface had not declared); the inner(action: any) => this.props.dispatch(action)simplified to(action) => .... - Left: the three top-of-file
anys on the middleware (storeDispatch: any,logReducerAction(action: any),generalMiddleware = (store: any) => (next: any) => async (action: any)) require typing redux actions across many call sites — significant refactoring;handleOpenURL_(event: any)—ShareExtension.shareURLhas a pre-existing typing inconsistency (declared()=> stringbut assigned both as a function and a string literal) so typing the event would expose that unrelated bug; oneawait reduxSharedMiddleware(store, next, action, storeDispatch as any)— kept asanycast since the upstream signature usesDispatchand storeDispatch is already typed loosely. Reasons updated on the disable comments.
- Removed:
In progress — packages with files still containing disable comments (see grep summary): components/ (ExtendedWebView, NoteEditor, NoteList, ScreenHeader, SelectDateTimeDialog, app-nav, plugins/backgroundPage, screens/{ConfigScreen, LogScreen, Note}, side-menu-content), contentScripts/, services/plugins/PlatformImplementation, utils/{appReducer, types}.
Second batch:
components/ExtendedWebView/types.ts— 1 removed, 0 left.OnMessageEvent.data: any→string(consumersJSON.parseit). Required casting thedatafield inindex.jest.tsx's test mock toas string(the mock passes pre-stringify values), and forced a typing tweak elsewhere.components/ExtendedWebView/index.tsx— 2 removed, 0 left.postMessage(message: any)→unknown(it's JSON-stringified).(props.style as any)cast removed by switching to array-stylestyle={[{...inline...}, props.style]}.components/ExtendedWebView/index.jest.tsx— 1 removed, 0 left.additionalProps: any→Record<string, unknown>; kept inline comment explaining the HACK.components/screens/ConfigScreen/SettingsToggle.tsx— 1 removed, 0 left.value: any→boolean.components/NoteList.tsx— 1 removed, 1 left.dispatch: (action: any)=> void→Dispatch(from redux). Left:styles_: Record<string, StyleSheet.NamedStyles<any>>—NamedStyles<T>requiresTto be the same record being passed; tightening tounknownbreaks property access. Reason updated.components/app-nav.tsx— 1 removed, 2 left.dispatchtypedDispatch;screenswidened toRecord<string, { screen: ComponentType<any> }>(typed wrapper, screens still have heterogeneous props). Left:route: any(NAV action with heterogeneous payload across NAV_GO/NAV_BACK/etc.) and the innerComponentType<any>for screens. Reasons updated.components/SelectDateTimeDialog.tsx— 3 removed, 0 left. ReplacedPureComponent<any, any>withPureComponent<SelectDateTimeDialogProps, SelectDateTimeDialogState>(local interfaces capturingthemeId,shown,date,onAccept,onRejectand state shape).components/ScreenHeader/index.tsx— 12 removed, 1 left. Added localtype ScreenHeaderStyles = ReturnType<typeof StyleSheet.create>and replaced every inner button factory'sstyles: anyparameter with it (12 occurrences). Left:styleObjectbuilder usesRecord<string, any>because it incrementally mixesViewStyle,TextStyle, andIconStyleentries spread from theme.icon — splitting into typed sub-objects would force restructuring. Reason updated.components/screens/ConfigScreen/SettingComponent.tsx— 3 removed, 0 left.value: any→unknown(callers pass arbitrary setting values; the inner branches narrow before forwarding).output: any = null→React.ReactElement | null = null.items as any(for the Dropdown options fromenumOptionsToValueLabels) →items as unknown as DropdownListItem[]becauseenumOptionsToValueLabelsreturns{ [computedKey]: string }[]where the runtime values happen to belabel/valuebut the type system can't tell. Forwarding toSettingsToggle/ValidatedIntegerInput/SettingTextInputadds explicit narrowing (!!props.value,props.value as number,props.value as string) per Setting type.components/screens/ConfigScreen/SectionSelector/index.tsx— 1 removed, 0 left.settings: Record<string, any>→Record<string, unknown>(the localSettingsMaptype isn't exported from@joplin/lib/components/shared/config/config-shared; the function accepts a wider record).components/screens/LogScreen.tsx— 2 removed, 1 left.navigation: any→{ state: { defaultFilter?: string } }(the only field accessed).navigationOptions(): any→: { header: null }. Left:styles: anybuilder — same heterogeneous-style-record pattern asScreenHeader.styleObject. Reason updated.components/screens/ConfigScreen/ConfigScreen.tsx— touched (not yet processed for disable count, butrenderToggle'svalue={value}→value={!!value}to satisfy the narrowedSettingsToggle.value: boolean).
Verification at checkpoint: package yarn tsc --noEmit clean; yarn linter-ci packages/app-mobile/ clean. Current state: 131 → 71 disable comments (60 removed).
Third batch (completing the package):
components/screens/ConfigScreen/ConfigScreen.tsx— 12 removed, 4 left.navigation: any→{ state?: { sectionName?: string } };navigationOptions(): any→: { header: null };onHeaderLayout/onSectionLayout/inlineonLayoutevents →LayoutChangeEvent(imported fromreact-native);renderButton/addSettingButtonoptions →{ description?: string; statusComp?: ReactElement; disabled?: boolean };handleSetting/settingToComponent/innerupdateSettingValuevalue: any→unknown;renderFeatureFlags'soutput: any[]→ReactElement[]. Left:settings: Record<string, any>on both state and props (and the parameters that take settings) — heterogeneous values (string/number/boolean/object) accessed by string key across many call sites;unknownforces casts at every read. Reason updated.components/screens/Note/Note.tsx— 13 removed, 4 left.emptyArray: any[]→never[];lastSavedNote: any→NoteEntity | null;styles_: any→Record<string, ReturnType<typeof StyleSheet.create>>;noteTagDialog_closeRequested: any→()=> void;refreshResource(resource: any)→ResourceEntity;focusUpdateIID_: any→ReturnType<typeof setTimeout> | null;folderPickerOptions_: any→FolderPickerOptions(imported from../../ScreenHeader);navigationOptions(): any→: { header: null };setState((state: any))→ inferred;onMarkForDownload(event: any)→{ resourceId: string };onPlainEditorSelectionChange(event: NativeSyntheticEvent<any>)→NativeSyntheticEvent<{ selection: SelectionRange }>;saveOneProperty(value: any)→unknown. Left:editorRef: any(union of Markdown/RichText/NoteBody viewers, each with different command surfaces);menuOptionsCache_: Record<string, any>(heterogeneous command/option entries);dialogbox: any(react-native-dialogbox ref, no library types);styles: anybuilder (heterogeneous view/text/icon style entries). Reasons updated.components/side-menu-content.tsx— 1 removed, 1 left.menuItems: any[]→PromptButtonSpec[](imported from./DialogManager/types). Left:syncReport: any— matchesstate.syncReport: anyin@joplin/lib/reducerandSynchronizer.reportToLines(report: any); tightening here would require updating the lib types first. Reason updated.components/NoteEditor/NoteEditor.tsx— 1 removed, 0 left.execCommand(command, ...args: any[])→unknown[].components/NoteEditor/hooks/useEditorCommandHandler.ts— 1 removed, 0 left.(...args: any[])→unknown[]; theargs[0]?.name/args[0]?.argsreads cast through{ name?: string; args?: unknown[] }.services/plugins/PlatformImplementation.ts— 2 removed, 1 left.Components.[key: string]: any→unknown;registerComponent(component: any)→unknown. Left:get nativeImage(): any— matchesBasePlatformImplementation.nativeImage: any. Reason updated.components/plugins/backgroundPage/initializePluginBackgroundIframe.ts— 1 removed, 0 left.(window as any).joplin = ...→(window as Window & { joplin?: unknown }).joplin = ....components/plugins/backgroundPage/utils/wrapConsoleLog.ts— 2 removed, 1 left.originalLog as any→as ((...args: unknown[])=> void) | undefined; outer wrapper signature(...args: any[])→unknown[]. Left: the} as any;cast at end ofwrapLogFunction— assigning a generic(...args: unknown[])=> voidto a specific console method requires going throughanybecause TypeScript treatsconsole[key]'s type as the intersection of all method signatures. Reason updated.components/NoteEditor/ImageEditor/ImageEditor.tsx— 1 removed, 0 left.onError(event: any)→WebViewErrorEvent(imported fromreact-native-webview/lib/WebViewTypes).contentScripts/markdownEditorBundle/utils/useCodeMirrorPlugins.ts— 1 removed, 0 left.postMessageHandler(message: any): Promise<any>→unknown/Promise<unknown>(matchesContentScriptData.postMessageHandler: (message: unknown)=> unknownin@joplin/editor/types).contentScripts/rendererBundle/useWebViewSetup.ts— 1 removed, 0 left.pluginOptions: any→PluginOptions(imported from@joplin/renderer/MarkupToHtml); innerenabled: subValues[n]→enabled: !!subValues[n](PluginOptions expects boolean).contentScripts/rendererBundle/contentScript/Renderer.test.ts— 1 removed, 0 left.pluginSettings: Record<string, any>→Record<string, unknown>.contentScripts/imageEditorBundle/contentScript/index.test.ts— 1 removed, 0 left.window.ResizeObserver = class { ... } as any— added the missingunobserve()anddisconnect()methods so the class satisfies theResizeObserverinterface without a cast.root.tsx— 0 removed, 6 left (reasons updated).storeDispatch: any/logReducerAction(action: any)/generalMiddleware = (store: any) => (next: any) => async (action: any)/reduxSharedMiddleware(...storeDispatch as any)/handleOpenURL_: any— all kept asanybecause redux actions in this codebase don't have a single typed union; tightening would require a coordinated rewrite of every dispatched action across the mobile app. Reasons updated on every disable comment.utils/appReducer.ts— 0 removed, 8 left (reasons updated). The 3Old code before rule was appliedentries got their reason rewritten; the 5Assigning types... would be too big of a refactoringentries were already correctly tagged.utils/types.ts— 0 removed, 2 left (reasons updated).AppState.routeandAppState.noteSideMenuOptions— same redux NAV / per-screen heterogeneous-payload story as appReducer.utils/fs-driver/fs-driver-rn.ts— 0 removed, 1 left (reason already documented in the first batch).components/app-nav.tsx— 0 removed, 2 left (reasons documented in the second batch).components/NoteList.tsx— 0 removed, 1 left (StyleSheet.NamedStyles<any>— reason documented in the second batch).components/ScreenHeader/index.tsx— 0 removed, 1 left (heterogeneousstyleObjectbuilder — reason documented in the second batch).components/screens/LogScreen.tsx— 0 removed, 1 left (heterogeneousstylesbuilder — reason documented in the second batch).
Verification: package yarn tsc --noEmit clean; yarn linter-ci packages/app-mobile/ clean; root yarn tsc --noEmit (all workspaces) clean.
Summary: 131 → 33 disable comments (98 removed). The 33 remaining all fall into a small set of "structural" categories:
- Redux action/route shapes across
root.tsx(6),appReducer.ts(8),utils/types.ts(2),app-nav.tsx(2) — total 18. Tightening requires a discriminated action union across the mobile codebase. - Heterogeneous style-builder objects in
ScreenHeader/index.tsx(1),LogScreen.tsx(1),Note.tsx(1) — total 3. Each mixes ViewStyle/TextStyle/IconStyle entries built incrementally. - Heterogeneous redux state fields tied to lib's loose typing:
syncReport: anyinside-menu-content.tsx(1);nativeImage: anyinPlatformImplementation.ts(1) — total 2. Tightening requires updating the lib base types first. - Per-screen ConfigScreen settings:
settings: Record<string, any>inConfigScreen.tsx(4 occurrences: state, props,sectionToComponent,renderFeatureFlags) — total 4. Tightening tounknownforces casts at everysettings[key]read across the file. - Per-component fields that the library doesn't type:
editorRef/dialogbox/menuOptionsCache_/stylesbuilder inNote.tsx(4) — total 4. NamedStyles<any>inNoteList.tsx(1) — TypeScript pattern limitation.console[key] = ... as anyinwrapConsoleLog.ts(1) — TypeScript pattern limitation (intersection of console method signatures).output: any[]infs-driver-rn.ts(1) —readDirStatsoutput mixes SAFDocumentFileDetailand normalizedStat-shaped entries.
packages/server
Session date: 2026-05-12
Starting baseline 2026-05-12 matches the progress doc: 227 disable comments across 67 files, all tagged Old code before rule was applied.
General observation (different from prior packages): many of the server's anys sit at junction points (Koa context, Knex query callbacks, view contents, error payloads) where tightening propagates through many call sites. So this package will have a much lower remove-rate than the front-end packages.
First batch:
utils/strings.ts— 1 removed, 0 left.yesOrNo(value: any)→unknown.utils/array.ts— 1 removed, 1 left.removeElementtyped generically as<T>(array: T[], element: T). Left:unique(array: any[])— tried generic<T>, butBaseModel.loadByIdscalls it withstring[] | number[]and TS can't unifyTacross the union, forcing a cast at the call site (a wider blast radius). Reason updated.utils/cache.ts— 4 removed, 0 left.CacheEntry.object: any→string(always JSON-stringified);setAny/setObjecto: any→unknown;getAnyreturnPromise<any>→Promise<unknown>(existingas objectcast at the one public consumer remains valid).utils/errors.ts— 3 removed, 0 left.ErrorOptions.details?: any→unknown;ApiError.details: any→unknown;errorToPlainObject(error: any)→unknownwith'httpCode' in errornarrowing followed by(error as { httpCode?: number }).httpCodecasts on each field read (TS'sinoperator doesn't narrowunknownto a typed shape, only toobject).services/MustacheService.ts— 1 removed, 1 left. The locallayoutView: anyinrenderView→Record<string, unknown>. Left:View.content?: any— tried tightening toRecord<string, unknown>, butrouteHandler.ts:61readsview.content.error.httpCodeand other dynamic paths; views contribute heterogeneous content shapes per route. Reason updated.
Files attempted but reverted (still any):
commands/BaseCommand.ts—run(argv: any). Triedyargs.Arguments, but subclasses (e.g.CompressOldChangesCommand,StorageCommand) narrowargvto a per-commandArgvinterface, and TS function-parameter contravariance forbids that without making the whole class generic — which would propagate through everyBaseCommand[]consumer. Reason updated.utils/urlUtils.ts—setQueryParameters(query: any). Callers pass KoaParsedUrlQuery(Record<string, string | string[]>), pagination shapes with numbers, and plain string records. Tightening forces fixes at every call site. Reason updated.config.ts—initConfig(overrides: any). TriedPartial<Config>, butConfig.resourceDir: stringis required and only set by some test overrides; the existing spread relies onanyto bypass the missing-field issue. Reason updated.
Verification at checkpoint: package yarn tsc --noEmit clean. 227 → 217 disable comments (10 removed).
Second batch:
models/KeyValueModel.ts— 3 removed, 0 left. The twoas anycasts invalue<T>use the existing type parameter (as T). The localvalue: anyinreadThenWritebecomesawait this.value<Value>(key)— the explicit type param uses the publicValue = number | stringalready defined in the file.models/BackupItemModel.ts— 1 removed, 0 left.add(content: any)→string | Buffer(the only runtime caller passes a JSON string; the storage type isBuffer). Inner assignment usescontent as Buffer.models/UserItemModel.ts— 1 removed, 0 left. Droppedas anyonloadByIds(options.byUserItemIds as any)—byUserItemIdsis already typednumber[]andloadByIdsacceptsstring[] | number[].models/UserDeletionModel.ts— 0 removed, 1 left.end(error: any): triedError, but tests pass plain strings. TriedError | string, buterrorToStringrequiresError; wrapping strings innew Error()changes runtime output (adds astackfield to the serialized payload). Reason updated.models/utils/pagination.ts— 4 removed, 0 left.requestPaginationOrder(query: any)→ParsedUrlQuery | PaginationQueryParamswithas string/as PaginationOrderDirnarrowing on the read fields;requestPagination(query: any)→(Pagination & PaginationQueryParams) | null;filterPaginationQueryParams(query: any)→PaginationQueryParams | null;paginateDbQuerymade generic over<T = unknown>forPaginatedResults<T>and the localorderSql: any[]inferred from.map.utils/views/table.ts— 2 removed, 0 left.Table.requestQuery?: any→PaginationQueryParams;makeTablePagination(query: any)→ParsedUrlQuery(imported fromquerystring).utils/views/select.ts— 0 removed, 2 left. TriedRecord<string, unknown>, but callers pass concrete entity types likeUser(no index signature). Reason updated.models/ChangeModel/ChangeModel.ts— 1 removed, 0 left.requestDeltaPagination(query: any)→ChangePagination | null.models/ShareModel.ts— 2 removed, 0 left.shareUrl(query: any)→Record<string, string | number>;itemCountByShareIdPerUser'sgroupBy('user_id') as any→as unknown as { item_count: number; user_id: Uuid }[](Knex's typed builder doesn't carry the aggregate column shape throughdb.raw).utils/testing/koa/FakeRequest.ts— 2 removed, 0 left. Introduced localFakeNodeRequest { method?: string }(the only field used).utils/testing/koa/FakeResponse.ts— 4 removed, 0 left.body: any→unknown;headers_: any→Record<string, string>;set/getparams/return →string.utils/testing/fileApiUtils.ts— 2 removed, 0 left.getDeltareturn type and inner castPaginatedResults<any>→PaginatedResults<unknown>.models/items/storage/testUtils.ts— 1 removed, 0 left.let error: any = null→Error & { code?: CustomErrorCode }.
Verification at checkpoint: package yarn tsc --noEmit clean. 227 → 193 disable comments (34 removed).
Third batch:
utils/prettycron.ts— 17 removed, 0 left. Allnumbers: any[]→number[];numberToDateName(value: any, type: any)→(value: number | string, type: 'dow' | 'mon')(the function doesvalue - 1, so wrapping inNumber());dateList(numbers: any[], type: any)→(numbers: number[], type: 'dow' | 'mon'); introduced localtype LaterSchedule = Record<string, number[]>and used it forremoveFromSchedule,scheduleToSentence;removeFromSchedule(schedule, member: any, length: any)→(LaterSchedule, string, number); the fourcronspec: any/numDates: anyhandlers typed asstring/number; the final(window as any).prettyCroncast tightened to(window as Window & { prettyCron?: Record<string, unknown> }).prettyCron.utils/routeUtils.test.ts— 4 removed, 0 left. ThreetestCases: any[]typed as tuple arrays ([string, string, string, ItemAddressingType][],[string, {...}][],[string, string[]][]).routes: Record<string, any>typedRecord<string, number>with threeas unknown as Parameters<typeof findMatchingRoute>[1]casts at call sites (the test injects numbers in place ofRouterinstances).routes/api/sessions.test.ts— 8 removed, 0 left. Eight(context.response.body as any).idcasts →as { id: string }.routes/api/items.test.ts— 4 removed, 0 left.tree: any→Record<string, Record<string, null>>;PaginatedResults<any>(×2) →<unknown>;result.items as any→as unknown as SaveFromRawContentResult.routes/api/shares.test.ts— 3 removed, 0 left.tree: any→Record<string, Record<string, null>>; bothPaginatedResults<any>→<Share>and<{ user: { email: string }; status: ShareUserStatus }>(the test only reads those fields).routes/index/users.test.ts— 3 removed, 0 left.postUser(props: any)→Partial<User>;patchUser(user: any)→Partial<User> & Record<string, unknown>;as any).valueon aquerySelector→querySelector<HTMLInputElement>('input[name=email]').value.routes/admin/users.test.ts— 2 removed, 0 left.postUser(props: any)/patchUser(user: any)typedRecord<string, unknown>(tests intentionally passmax_item_size: ''whichPartial<User>would reject).routes/index/stripe.test.ts— 2 removed, 0 left.WebhookOptions.stripe?: any→ReturnType<typeof mockStripe>;simulateWebhook(object: any)→Record<string, unknown>.routes/index/shares.link.test.ts— 2 removed, 0 left.getShareContent(query: any)→Record<string, string>; the inneras anyreturn cast →as string | Buffer.routes/api/share_users.ts— 2 removed, 0 left.bodyFields<any>→bodyFields<{ status?: number }>;items: any[]→Record<string, unknown>[].routes/api/share_users.test.ts— 1 removed, 0 left.PaginatedResults<any>→<{ share: { id: string } }>.
Verification at checkpoint: package yarn tsc --noEmit clean; spellcheck clean. 227 → 145 disable comments (82 removed).
Fourth batch:
routes/api/batch.ts— 3 removed, 0 left.SubRequest.body: any/SubRequestResponse.body: any→unknown;SubRequestResponse.header: Record<string, any>→Record<string, unknown>.routes/api/batch_items.ts— 2 removed, 0 left.PaginatedResults<any>→<unknown>; the inneras anycast →as unknown as unknown[].models/UserModel.test.ts— 3 removed, 0 left. The threesyncInfo*: anytest fixtures share a single inline object-shape type with optionalppk(the third variant deletes it).routes/index/login.ts— 1 removed, 0 left.makeView(error: any)→Error | null.routes/index/home.test.ts— 1 removed, 0 left.context.response.body as any→as string.routes/index/items.test.ts— 1 removed, 0 left.items: any→Record<string, Record<string, never>>.models/items/storage/StorageDriverS3.ts— 2 removed, 0 left. Introduced localReadableLikeinterface (only the 3 listener overloads used) sostream2bufferis typed; the S3 SDK return is an opaque union, so cast at the call site:stream2buffer(response.Body as ReadableLike).models/items/storage/StorageDriverS3.test.ts— 1 removed, 0 left.parse: any→StorageDriverConfig & { enabled?: boolean }.routes/api/users.ts— 1 removed, 0 left.bodyFields<any>→bodyFields<Partial<User>>(fromApiInputaccepts a partial user).routes/api/users.test.ts— 1 removed, 0 left.results: any→getApi<{ items: User[] }>.routes/api/ping.test.ts— 1 removed, 0 left.body as any→as { status: string; message: string }.models/LockModel.test.ts— 2 removed, 0 left.'wrongtype' as any→as unknown as LockType(and the same forLockClientType).db.migrations.test.ts— 2 removed, 0 left.dbSchemaSnapshotreturn →Awaited<ReturnType<typeof sqlts.toTypeScript>>; thedb as anycast →as unknown as Parameters<typeof sqlts.toTypeScript>[1].utils/testing/testUtils.ts— 4 removed, 3 left.createItemTree(tree: any)→Record<string, unknown>;createItemTree3(tree: any[])→ localItemTree3Nodeinterface;checkContextError'sbody: any→ cast throughas { code?: ErrorCode };setupAppContext({} as any, ...)→as unknown as AppContext. Left:AppContextTestOptions.request: any(httpMocks.RequestOptionsis too narrow; callers passfiles: { file: { path: string } }and free-form bodies);appContext: anyinsidekoaAppContext(intentionally mocks only a subset ofAppContext, cast at return); thecreateBaseAppContextone was removed. Reasons updated on the two remaining ones.utils/requestUtils.ts— 2 removed, 7 left. Two safe removals: the outerIncomingMessagecast informParseusesas unknown as FormParseRequest; thebodyFields/bodyFilesreq: anytypedIncomingMessage. The other entries (BodyFields,FormParseResult.files,FormParseRequest.body,convertFieldsToKeyValuereturn) were attempted but reverted —Record<string, unknown>breaksFields/Filescompatibility (formidable'sFile | File[]union surfaces.filepathaccess errors), andBodyFieldswidening propagates to every route handler that readsbody.fields.emailetc. without narrowing. Reasons updated on the remaining ones.utils/routeUtils.ts— 5 removed, 1 left.Response.response: any/constructor(response: any)→unknown;internalRedirect(...args: any[])→unknown[];ExecRequestResult.response: any→unknown;respondWithItemContent(koaResponse: any)→ localKoaResponseLikeinterface with justbodyandset(). Left:RouteHandler's...args: any[], Promise<any>— concrete handlers (login, mfa, users) narrowargsto per-route field types; tightening propagates through every route. Reason updated. (Plus a downstream castresponseObject.response as typeof ctx.responseinrouteHandler.ts:56for the now-unknownresponse.)models/ChangeModel/ChangeModel.test.ts— 1 removed, 0 left.itemsToCreate: any[]→{ id: string; children: never[] }[].models/ChangeModel/ChangeModel.old.ts— 1 removed, 0 left.Knex.Raw<any>→Knex.Raw<unknown>.routes/api/items.ts— 1 removed, 0 left.bodyFields.items.map((item: any))→(item: { name: string; body?: string }).tools/generateTypes.ts— 1 removed, 0 left.'pascal' as any→as Config['tableNameCasing'].models/utils/pagination.test.ts— 2 removed, 0 left.testCases: any→[Record<string, unknown> | null, Pagination][]; the inlineinput: anyremoved by inferring from the tuple. Inner literaldir: 'asc'switched toPaginationOrderDir.ASC.
Verification at checkpoint: package yarn tsc --noEmit clean; spellcheck clean. 227 → 102 disable comments (125 removed).
Fifth batch:
utils/joplinUtils.ts— 11 removed, 1 left. TightenedFileViewerResponse.body/ResourceInfo/LinkedItemInfoto concrete shapes (Buffer | string,NoteEntityetc.);unserializeJoplinItem/serializeJoplinItemtyped againstNoteEntity;getResourceInfosoutput uses the namedResourceInfosalias;jopItemanditemToRenderuseNoteEntity & { ...optional fields };FileToRender.content→Buffer | null(thenull as anycast is no longer needed). Left: therenderOptions: anyformarkupToHtml.render—@joplin/renderer'sRenderOptionsis loosely typed in that package; tightening would require updating renderer first.db.ts— 14 removed, 0 left.ConnectionCheckResult.error/latestMigration: any→Error | nulland{ name: string } | null; the slow-query handlerconnection: any, bindings: any[]→DbConnectionandunknown[]; the innerqueryInfos: Record<any, QueryInfo>andtimeoutId: any→Record<string, QueryInfo>andReturnType<typeof setTimeout>;filterBindings(bindings: any[]): Record<string, any>→unknown[]/Record<string, unknown>;KnexQueryErrorData.bindings: any[]→unknown[];migrateList'smigrations: anytyped as the actual[string | { file } | { name | file }][]tuple via aMigrationInfoalias;isNoSuchTableError/isUniqueConstraintErrorerror: any→{ code?: string; message?: string } | null | undefined. ThepgsetTypeParsercallback'sval: any→string.utils/testing/apiUtils.ts— 11 removed, 0 left. All thebody: Record<string, any>parameters (×9 functions) →body: object(entity types likeUser,FormUserdon't have an index signature, soRecord<string, unknown>rejects them —objectaccepts both concrete entities and plain records). Thequery: Record<string, any>→Record<string, unknown>(callers always pass plain objects).
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean. 227 → 67 disable comments (160 removed).
Sixth batch:
models/ItemModel.ts— 4 removed, 3 left.SaveFromRawContentResultItem.error→ union ofError & { httpCode?: number; code?: string }/PlainObjectError/null(callers read.httpCodeon it;errorToPlainObjectis also assigned to it).objectToApiOutputinneras any[k]casts →(output as Record<string, unknown>)[k].allForDebugtyped(Omit<Item, 'content'> & { content?: string | Buffer })[]and now spreads instead of mutating. Left:itemToJoplinItem(): any(heterogeneous Note/Folder/Resource/Tag return); the matchingjoplinItem?: anyfield; the innerjoplinItem: anylocal. Reasons updated.models/BaseModel.ts— 7 removed, 0 left.SaveOptions.validationRules/previousItem,DeleteOptions.validationRules,ValidateOptions.rules→Record<string, unknown>.all()rows: any[]cast →as T[]only.fromApiInputlocaloutput: any→Record<string, unknown>withas Record<string, unknown>on the input spread andas Ton the return.isNew's'id' in (object as any)→typeof object === 'object' && object && 'id' in objectnarrowing.app.ts— 7 removed, 0 left.defaultEnvVariables: Record<Env, any>→Record<Env, Partial<EnvVariables>>.markPasswords(o: Record<string, any>)→objectparameter (concrete entity types likeDatabaseConfiglack index signatures) with an internal cast toRecord<string, unknown>for iteration.getEnvFilePath(argv: any)→{ envFile?: string }.argv: Argv = yargsArgv as any→as unknown as Argv. ThecommandArgv._cast →as Argv & { _: string[] }.main().catch((error: any))→(error: Error).utils/testing/testRouters.ts— 3 removed, 2 left.execcallbackerror/stdout/stderr: any→(Error & { signal?: string }) | null/string/string.serverProcess: any→ReturnType<typeof spawn>.checkAndPrintResult(result: any)→unknownwithinnarrowing. Left:curlreturn type (it parses heterogeneous JSON responses that callers read without narrowing) and theresponse: anyinmain()(same reason). Reasons updated.utils/testing/shareApiUtils.ts— 5 removed, 0 left. Introduced localLegacyTreeNodeandShareTreeNodeinterfaces (with[key: string]: unknownonShareTreeNodeso test fixtures can include arbitrary fields liketitle).convertTree(tree: any)/createItemTree3(tree: any[])/shareFolderWithUser(itemTree: any)use those types.routes/admin/users.ts— 5 removed, 0 left.boolOrDefaultToValue/intOrDefaultToValue/makeUserfields: any→Record<string, unknown>. The field reads inmakeUsercast viaas string/as numberper User shape.error: anyonadmin/users/:idroute handler →Error | null.accountTypeOptions().map((o: any))→(o: { value: number; selected?: boolean }). TheformParse().fieldsis castas unknown as Record<string, unknown> & { id?: Uuid }at the makeUser call.
Verification at checkpoint: package yarn tsc --noEmit clean; lint clean. 227 → 36 disable comments (191 removed).
Final batch:
routes/index/stripe.ts— 4 removed, 0 left.stripeEvent(req: any)→IncomingMessage;StripeRouteHandlerreturn →Promise<unknown>; the two(postHandlers as any)[path.id]casts collapsed into a single typed lookup.env.ts— 4 removed, 0 left.parseEnv(defaultOverrides: any)→Partial<EnvVariables>. The three(output as any)[key]writes go through a singleoutputAsRecord = output as unknown as Record<string, unknown>alias.routes/index/users.ts— 3 removed, 0 left.makeUser(fields: any)→Record<string, unknown>withas stringnarrowing on field reads;error: anyonusers/:idGET →Error | null;accountTypeOptions().map((o: any))→(o: { value: number; selected?: boolean }).models/UserModel.ts— 2 removed, 1 left.(resource as any)[key]/(previousResource as any)[key]→as Record<string, unknown>.syncInfo(): Promise<any>→Promise<{ ppk?: { value: PublicPrivateKeyPair } }>. Left:checkMaxItemSizeLimit(joplinItem: any)— same heterogeneous itemToJoplinItem return as ItemModel.middleware/routeHandler.ts— 1 removed, 0 left.const r: any = { error }→ typed{ error: string; stack?: string; code?: string }.
Verification: package yarn tsc --noEmit clean; yarn linter-ci packages/server/src/ clean; root yarn tsc --noEmit (all workspaces) clean.
Summary: 227 → 22 disable comments (205 removed). Zero remaining Old code before rule was applied disables — all 22 left have descriptive reasons explaining why they can't be tightened. They fall into these categories:
- Heterogeneous redux/Koa shapes (5):
BaseCommand.run(argv: any)(per-command Argv narrowing forbids the base type from being typed without making the class generic);routeUtils.RouteHandler(per-route argument types);joplinUtils.renderOptions(renderer package's RenderOptions is loose);MustacheService.View.content(each view contributes different fields);testUtils.appContext(Koa mock only provides a subset). - Heterogeneous Joplin item types (3):
ItemModel.itemToJoplinItemplus the twojoplinItem: anylocals it feeds;UserModel.checkMaxItemSizeLimit.joplinItem. - Heterogeneous error shapes (1):
UserDeletionModel.end(error: any)— tests pass strings while runtime callers pass Errors. - Concrete entity types without index signatures (3):
urlUtils.setQueryParameters(query: any);select.tsyesNoOptions/yesNoDefaultOptions;app.tsconfig initConfig overrides. - Loose lib-typed defaults (1):
config.tsinitConfig(overrides: any)—Partial<Config>requiresresourceDir. - Heterogeneous test fixtures / API call results (3):
testRouters.curlandresponseinmain(parsed JSON responses);array.tsunique(TS can't unifyTacrossstring[] | number[]). - TypeScript pattern limitations (6):
testUtils.AppContextTestOptions.request; the 4 inrequestUtils.ts(BodyFields,FormParseResult.files,FormParseRequest.body,convertFieldsToKeyValue— all centered on formidable'sFields | Filesunion not allowing narrowing).
packages/app-cli
Session date: 2026-05-13
Files processed:
app/command-cp.ts— 1 removed, 0 left. Typedaction({ note, notebook? }).app/command-mv.ts— 1 removed, 0 left. Typedaction({ item, notebook }).app/command-ren.ts— 1 removed, 0 left. Typedaction({ item, name }).app/command-rmbook.ts— 1 removed, 0 left. Typedaction({ notebook, options? }).app/command-rmnote.ts— 1 removed, 0 left. Typedaction({ 'note-pattern', options? }).app/command-restore.ts— 1 removed, 0 left. Typedaction({ pattern }).app/command-edit.ts— 1 removed, 0 left. Typedaction({ note }).app/command-import.ts— 1 removed, 0 left. Typedaction({ path, notebook?, options }); outputFormat assignment now requiresas ImportModuleOutputFormat.app/command-mkbook.ts— 1 removed, 0 left. Typedaction({ 'new-notebook', options }).app/command-help.ts— 1 removed, 0 left. Typedaction({ command? }).app/command-use.ts— 1 removed, 0 left. Typedaction({ notebook }).app/command-attach.ts— 1 removed, 0 left. Typedaction({ note, file }).app/command-cat.ts— 1 removed, 0 left. Typedaction({ note, options }).app/command-geoloc.ts— 1 removed, 0 left. Typedaction({ note }).app/command-apidoc.ts— 1 removed, 0 left. Typedaction({ file }).app/command-done.ts— 2 removed, 0 left. TypedhandleAction(args: { note })andaction(args: { note }).app/command-set.ts— 2 removed, 0 left. Typedaction(args: { note, name, value? });newNote: any→Record<string, unknown>(still accepted byNote.save).app/command-ls.ts— 1 removed, 1 left. Typedactionargs; leftqueryOptions: anybecause it is fed to bothFolder.all(FolderLoadOptions) andNote.previews(PreviewsOptions) and the union has fields that neither type carries (caseInsensitive,orderBy). Comment reason updated.app/setupCommand.ts— 2 removed, 0 left. Typedcmd: BaseCommand, introduced localPromptOptions; the dispatcher arg is inferred fromBaseCommand.setDispatcher(fn: DispatcherFn)(added in base-command.ts edit), eliminating the lastanyhere. Side fix:App.setupCommand(cmd: string)was a wrong-pre-existing-annotation, now typedBaseCommand.app/command-e2ee.ts— 2 removed, 0 left. Typedactionargs; typedaskForMasterKey(error: { masterKeyId }).app/command-config.ts— 3 removed, 0 left.chunks: any→Buffer[]; typed action args;Record<string, any>→Record<string, unknown>for resultObj.app/command-export.ts— 3 removed, 0 left. Typed action args;n.idmap callbacks now infer entity types fromloadItems; format assignment usesExportModuleOutputFormat.Jex.app/gui/FolderListWidget.ts— 3 removed, 0 left. Introduced localFolderListItem = FolderEntity | TagEntity | SearchItem | '-'foritemRendererandnewItems; replaced(this.folders[i] as any).note_countwithFolderEntity & { note_count?: number }.app/command-settingschema.ts— 4 removed, 0 left. Typed action args;Record<string, any>→Record<string, unknown>for schema, props; removed unusedv: anyannotation.app/gui/StatusBarWidget.ts— 2 removed, 2 left. Typedprompt(promptString: string, options)andtextStyle: (s: string) => s. Left 2anyfor tkwidgets terminal-kitinputFieldoptions/callback — no published types and tkwidgets has no .d.ts. Comments updated with reason.app/LinkSelector.ts— 5 removed, 0 left. Introduced localTextWidgetinterface for tk text widget shape; replaced(lines[i] as any).matchAllwith direct call (String.prototype.matchAllis well-typed).app/command-sync.ts— 3 removed, 2 left. Typedactionargs,log(...s: string[]). Leftoptions: anyandreport: anybecauseSynchronizer.start(options: any)andSynchronizer.reportToLines(report: any)in lib are themselvesany— tightening here would diverge from lib. Comments updated.app/command-testing.ts— 5 removed, 0 left.randomElement→ generic<T>(array: T[]): T | null;itemCount(args: { arg0 });options(): [string, string][]; typedactionargs;promises: Promise<unknown>[].app/app.ts— 0 removed, 6 left. All disables had descriptive reasons (Dynamic command loading system,Dynamic command metadata,Dynamic command type,Dynamic GUI type with many optional methods,Redux dispatch type requires AnyAction) — none wereOld code before rule was applied, so out of scope per rule 3. Side fix in checkpoint 1:App.setupCommand(cmd: string)annotation corrected toBaseCommand.app/services/plugins/PluginRunner.ts— 6 removed, 0 left.wrapper: any→Record<string, (...args: unknown[]) => unknown>with final cast totypeof Consoleto matchSandboxProxy.console; arg arrays →unknown[];activeSandboxCalls_: any→Record<string, boolean>.app/base-command.ts— 4 removed, 4 left. Introduced typedStdoutFn/PromptFn/DispatcherFnaliases; typedstdout_/prompt_/dispatcher_fields; typedencryptionCheck(item: { encryption_applied? }). Leftaction(_args: any)andoptions(): unknown[]: parameters are contravariant so tightening base would break all subclass overrides. The 3 remaininganyare now centralized in the type aliases at the top of the file (with reasons): stdout accepts arbitrary message values, prompt response varies, dispatch action shape varies; tests pass sync mocks that don't return Promises.tests/HtmlToMd.ts— 1 removed, 0 left.htmlToMdOptions: any→ParseOptions(exported from@joplin/lib/HtmlToMd).tests/MdToHtml.ts— 3 removed, 0 left.newTestMdToHtml(options: any)→Partial<MdToHtmlConstructorOptions>; theResourceModelmock requiresas unknown ascast because it intentionally omitsfilenameandisSupportedImageMimeType.mdToHtmlOptionsalready had a real type, just a stale disable comment.pluginOptions: any→Record<string, { enabled: boolean }>.tests/testUtils.ts— 1 removed, 0 left.Record<string, any>→Record<string, unknown>onPluginServiceOptions.getState.tests/services/keychain/KeychainService.ts— 1 removed, 0 left.describeIfCompatible(fn: any, elseFn: any)→() => void.tests/services/plugins/PluginService.ts— 1 removed, 0 left.(f: any) => f.title→ inferredFolderEntity(fromFolder.all()).tests/services/plugins/api/JoplinWorkspace.ts— 2 removed, 0 left.appState: Record<string, any>→Record<string, string[]>;result: any→{ id: string; event: number }.tests/services/plugins/sandboxProxy.ts— 4 removed, 0 left.args: any[]→unknown[];target: any→ inferred function type (bothitblocks).
Verification: package yarn tsc --noEmit clean; yarn linter-ci packages/app-cli/ clean; root yarn tsc --noEmit (all workspaces) clean; spellcheck clean.
Summary: 90 → 16 disable comments (74 removed). Zero remaining Old code before rule was applied disables — all 16 left have descriptive reasons explaining why they cannot be tightened. They fall into these categories:
- Base-class contravariance (4):
base-command.ts—StdoutFn,PromptFn,DispatcherFnaliases at the top of the file (centralized) and theaction(_args: any)method. Parameters are contravariant; subclasses narrow per-command, so the base must stay permissive. Tests pass synchronous mocks that don't return Promises. - Heterogeneous query options (1):
command-ls.tsqueryOptionsis passed to bothFolder.all(FolderLoadOptions) andNote.previews(PreviewsOptions) and the union has fields neither type carries; splitting would be a structural refactor. - Loose lib-typed APIs (2):
command-sync.ts—Synchronizer.start(options: any)andSynchronizer.reportToLines(report: any)in lib are themselvesany; tightening here would diverge from lib. - No published types for JS modules (3):
StatusBarWidget(tkwidgets terminal-kitinputFieldoptions + callback) andcommand-sync.tsoneDriveApiUtils_(onedrive-api-node-utils.jsis plain JS). - Dynamic command/state shapes already documented (6): all of
app.tswas already annotated with descriptive non-"Old code" reasons before this pass —commands_,commandMetadata_,activeCommand_,gui_, dynamic command type at L175, and the redux dispatch type.