require() → import Migration Progress
Tracks the effort to migrate const X = require('...') to typed import statements so values pulled in from other modules carry real types instead of implicit any.
Goal
Replace const X = require('...') with a typed import whenever possible and the change is mechanical. require() is fine to keep when the imported module has no types — it already returns implicit any, so no : any or eslint-disable is needed. The aim is to narrow require to the cases where it's actually load-bearing, and let the type checker cover the rest.
Rules
For each const X = require('...'):
- Convert to a typed
importwhen types exist:- The module ships its own
.d.ts(checknode_modules/<pkg>/package.jsonfor atypesfield). - A matching
@types/<pkg>is already installed. - The module is an internal
@joplin/*package or a relative path to a.tsfile. - It's a Node built-in (
path,fs, etc.) — but confirm the installed@types/nodecovers the specific submodule (e.g.readline/promisesneeds ≥ 17).
- The module ships its own
- Pick the import form that matches the module's shape:
- Named exports:
import { foo, bar } from 'pkg'(use{ foo as bar }to rename rather than a separateconst). module.exports = Xstyle: withesModuleInterop: false(this repo's setting), useimport * as X from 'pkg'.import X from 'pkg'only works when the typings declare a real default export.
- Named exports:
- Leave the
require()in place when:- The module has no types available — migrating would force a
// @ts-ignore, a newdeclare moduleshim, or a fresh@types/*install. - There's an explicit comment explaining why
require()is needed (RN bundler compatibility, conditional / lazy / side-effect load, dynamic path). - It's part of a
: typeof X = require(...)pattern already typed via a pairedimport type— a deliberate runtime workaround. - Converting would require non-mechanical refactoring.
- The module has no types available — migrating would force a
- Don't install new
@types/*packages or upgrade dependencies as part of this migration. The aim is to surface existing types, not expand the dependency graph. - Don't make whitespace-only changes to surrounding code (per
CLAUDE.md). - Don't add explanatory comments unless the why is non-obvious (e.g. "needs
import *because the package usesmodule.exports = ..."). - After each package, run
yarn tsc --noEmitfrom the package directory to verify nothing broke. The pre-commit hook handles linting and spellcheck.
Files to never touch
packages/generator-joplin/generators/app/templates/**— Yeoman template; package lacks atypescriptdep.packages/app-cli/tests/support/plugins/*/api/types.tsand similar — regenerated plugin API copies.- Anything under
**/node_modules/**or**/build/**.
Workflow
- One PR per package, smallest first.
- Update this file as you go, not at the end. Sessions can be auto-compacted or interrupted; the on-disk file is the source of truth.
- After each file: add an entry to the package's Per-package detail subsection.
- After each package: update the Status table row.
- For large packages (
lib,app-desktop,app-mobile), checkpoint the row every ~20 files.
- When resuming, re-read this file first and compare entries against the live state with:```bash grep -rn --include=".ts" --include=".tsx" -E "^\sconst\s+=+=\srequire(" packages/[name here]/
Trust the file, not memory.
## Status
Counts captured 2026-05-25, before any work. `const X = require(...)` occurrences in `*.ts`/`*.tsx` source files, excluding `node_modules/` and `build/`. Some will legitimately stay as `require()` after evaluation (untyped libraries, RN bundler workarounds, etc.) — the "Converted" column tracks how many were actually migrated.
| # | Package | Requires (start) | Converted | Remaining | Status |
| ----- | ----- | ----- | ----- | ----- | ----- |
| 1 | pdf-viewer | 1 | 0 | 1 | done (2026-05-25) |
| 2 | fork-uslug | 1 | 0 | 1 | done (2026-05-25) |
| 3 | default-plugins | 1 | 1 | 0 | done (2026-05-25) |
| 4 | editor | 2 | 0 | 2 | done (2026-05-25) |
| 5 | plugin-repo-cli | 2 | 0 | 2 | done (2026-05-25) |
| 6 | htmlpack | 3 | 3 | 0 | done (2026-05-25) |
| 7 | utils | 9 | 4 | 5 | done (2026-05-25) |
| 8 | renderer | 23 | 7 | 16 | done (2026-05-25) |
| 9 | server | 26 | 9 | 17 | done (2026-05-25) |
| 10 | tools | 49 | 29 | 20 | done (2026-05-25) |
| 11 | app-cli | 49 | 18 | 31 | done (2026-05-25) |
| 12 | app-mobile | 61 | 18 | 43 | done (2026-05-25) |
| 13 | app-desktop | 131 | 74 | 57 | done (2026-05-25) |
| 14 | lib | 195 | 55 | 140 | done (2026-05-25) |
| — | generator-joplin | 1 | — | — | excluded (template) |
Total in-scope `require()` calls at start: **558** (counted across `*.ts`/`*.tsx`, excluding `**/node_modules/**`, `**/build/**` and `**/dist/**`).
## Recommended order
Smallest first so the workflow stabilises before the large ones:
1. Warm-up: pdf-viewer, fork-uslug, default-plugins, editor, plugin-repo-cli, htmlpack (~10 requires total)
2. utils (9)
3. renderer (23)
4. server (27)
5. tools (49)
6. app-cli (54)
7. app-mobile (61)
8. app-desktop (131)
9. lib (195) — biggest; do last so prior packages inform the work
## Per-package detail
Each package gets a subsection added below when work begins. Format:
```markdown
## packages/<name>
Session date: YYYY-MM-DD
Files processed:
- path/to/file.ts — N converted, M left (reasons for skips)
Files skipped entirely:
- path/to/file.ts — reason
packages/pdf-viewer
Session date: 2026-05-25
Files skipped entirely:
- main.tsx —
react-dom/clienthas types and tsc passes the conversion, but the package's build scripts are all disabled (tsc: "",build: "") and the bundling pipeline that produces thevendor/lib/@joplin/pdf-viewer/artifact isn't traceable from this repo. The author explicitly choserequire()to fix a React 18 warning (commit 58da15432). Leave alone until the bundling path is understood.
packages/fork-uslug
Session date: 2026-05-25
Files skipped entirely:
- lib/uslug.ts —
node-emojihas no types installed.
packages/default-plugins
Session date: 2026-05-25
Files processed:
- build.ts — 1 converted (
yargs→import * as yargs from 'yargs').
packages/editor
Session date: 2026-05-25
Files skipped entirely:
- CodeMirror/utils/formatting/RegionSpec.ts — imports from
@joplin/lib/string-utils-common, which is a deliberate.jsfile (no.d.ts) per the comment at the top of that file. - CodeMirror/CodeMirror5Emulation/CodeMirror5Emulation.ts — same as above.
packages/plugin-repo-cli
Session date: 2026-05-25
Files skipped entirely:
- commands/updateRelease.ts —
node-fetchisrequire(...).defaultwith an explicit comment linking the known node-fetch issue;gh-release-assetshas no types installed.
packages/htmlpack
Session date: 2026-05-25
Files processed:
- packToWriter.ts — 1 converted (
html-entities). - index.ts — 1 converted (
datauri/sync— usesimport X = require(...)because the package usesexport = X). - utils/parseHtmlAsync.ts — 1 converted (
@joplin/fork-htmlparser2).
packages/utils
Session date: 2026-05-25
Files processed:
- Logger.ts — 2 converted (
moment,async-mutex), 1 left (sprintf-js— no @types installed). - html.ts — 2 converted (
html-entities,@joplin/fork-htmlparser2).
packages/renderer
Session date: 2026-05-25
Files processed:
- htmlUtils.ts — 2 converted (
html-entities,@joplin/fork-htmlparser2). - utils.ts — 1 converted (
html-entities). - highlight.ts — 1 converted (
highlight.js/lib/core). - MdToHtml.ts — 1 converted (
html-entities); 4 left:md5(no types),@joplin/fork-uslug(types describe{ default: fn }butindex.jsunwraps.default, so types are misaligned with runtime),markdown-it-anchor(no types),./defaultNoteStyle(JS, no.d.ts). - MdToHtml/renderMedia.ts — 1 converted (
html-entities). - MdToHtml/linkReplacement.ts — 1 converted (
html-entities); 2 left:../urlUtils.js(JS, no.d.ts),font-awesome-filetypes(no types).
Files skipped entirely:
- MarkupToHtml.ts —
markdown-itis paired withimport type * as MarkdownItType(deliberate workaround for mobile bundling, per the convention this plan skips). - HtmlToHtml.ts —
md5(no types). - MdToHtml/rules/katex.ts —
md5,json-stringify-safe(no types);./katex_mhchem.js(JS, no.d.ts). - MdToHtml/rules/highlight_keywords.ts —
md5(no types);../../stringUtils.js(JS). - MdToHtml/rules/sanitize_html.ts —
md5(no types). - MdToHtml/rules/link_open.ts —
../../urlUtils.js(JS). - MdToHtml/rules/fountain.ts —
../../vendor/fountain.min.js(vendor JS).
packages/server
Session date: 2026-05-25
Files processed:
- app.ts — 1 converted (
sqlite3); 2 left:@koa/cors(no types),@joplin/lib/shim-init-node.js(lib's.tssource usesmodule.exports = { ... }rather than ESexport, so a typedimport {}fails — would require modifying lib to convert). - utils/htmlUtils.ts — 1 converted (
html-entities). - utils/bytes.ts — 1 converted (
pretty-bytes, viaimport X = require()because the package usesexport =). - utils/joplinUtils.ts — 1 converted (
@joplin/lib/string-utils); 1 left (@joplin/lib/database-driver-node.js— JS only, no.d.ts). - utils/testing/populateDatabase.ts — 1 converted (
sqlite3); 1 left (shim-init-node, same reason as app.ts). - utils/routeUtils.ts — 1 converted (
@joplin/lib/path-utils). - utils/testing/testRouters.ts — 2 converted (
query-string,child_process.spawn); 1 left (inlinerequire('child_process').execinside a function — would require refactoring to top-level). - routes/admin/emails.ts — 1 converted (
@joplin/lib/string-utils).
Files skipped entirely:
- config.ts —
require(${__dirname}/packageInfo.js)uses a template-literal path; the line is also already typed via: PackageJson. - utils/crypto.ts, models/UserModel.ts, routes/index/mfa.ts —
thirty-two(no types). - utils/prettycron.ts —
dayjsand its plugins (advancedFormat,calendar) are convertible, but this is a vendored file (see its header comment) so kept untouched; converting also surfaces a pre-existingnoImplicitReturnsissue at line 153.laterhas no types. - utils/stripe.ts —
stripehas types, but the existing call site usesstripeLib(secretKey)as a function; the typedStripeis a class requiringnew Stripe(secretKey, config). Converting would force a logic/signature change. - services/TaskService.ts —
node-cron(no types). - routes/admin/tasks.ts —
prettycron(no types). - tools/generateTypes.ts — inline
require('fs')inside a function (would require refactoring to top-level).
packages/tools
Session date: 2026-05-25
Files processed:
- release-clipper.ts — 1 converted (
md5-file). - generate-database-types.ts — 2 converted (
@rmp135/sql-tsmerged with existing named import;fs-extra). - generate-images.ts — 2 converted (
md5-file,sharp). The typedsharpimport surfaced a latent issue at line 682 wheres = s.toFile(destPath)assigned aPromise<OutputInfo>to aSharpvariable, which fixed in the same follow-up: replaced withawait s.toFile(destPath). - build-release-stats.ts — 1 converted (
yargs-parser). - update-readme-contributors.ts — 1 converted (
./tool-utils.jsmerged into the existingimport { rootDir }); 1 left (request, no types). - release-android.ts — 3 converted (
path,uri-template,node-fetch). The typednode-fetchimport surfaced a latent issue at line 230 where'Content-Length': binaryBody.length(anumber) didn't matchHeadersInit'sstringrequirement; fixed in the same follow-up by wrapping inString(...). - website/utils/news.ts — 1 converted (
moment). - update-readme-sponsors.ts — 1 converted (
@joplin/lib/string-utils). - website/build.ts — 3 converted (
glob,path,md5-file). - tool-utils.ts — 3 converted at top level (
node-fetch,execa,moment); 7 inlinerequire()calls left inside functions (child_process.exec/.spawn,path,https,crypto,fs,readline) — converting needs moving to module top, which is a small refactor. - fuzzer/model/FolderRecord.test.ts — 1 converted (
sqlite3); 1 left (@joplin/lib/shim-init-node, samemodule.exports = { ... }issue as in server). - website/processDocs.ts — 4 converted (
md5-file,@joplin/fork-htmlparser2,style-to-js— drop.defaultsinceexport = Xis callable,crypto). - website/utils/applyTranslations.ts — 2 converted (
html-entities,@joplin/fork-htmlparser2). - website/utils/frontMatter.ts, website/utils/frontMatter.test.ts — 1 each (
moment). - website/utils/openGraph.ts — 1 converted (
@joplin/lib/string-utils). - website/buildTranslations.ts — 1 converted (
gettext-extractor).
Files skipped entirely:
- 6 inline
const argv = require('yargs').argv;calls insidemain-style async functions (postPreReleasesToForum.ts, tagServerLatest.ts, build-translation.ts, git-changelog.ts, release-android.ts, website/updateNews.ts) — would require a top-level import and a refactor to keep semantics; skipped per the inline-require policy. - fuzzer/cli.ts —
@joplin/lib/shim-init-node(same as above). - convertThemesToCss.ts —
require(${baseThemeDir}/${themeFile})is a dynamic template-literal path. - checkLibPaths.test.ts — inline
require('../lib/shim')inside a function. - utils/translation.ts —
gettext-parser(no types). - website/updateNews.ts —
rss(no types).
packages/app-cli
Session date: 2026-05-25
Files processed:
- tests/MdToHtml.ts — 2 converted (
@joplin/lib/path-utils,@joplin/lib/theme). - tests/HtmlToMd.ts — 2 converted (
os,@joplin/lib/path-utils). - app/command-version.ts — 2 converted (
@joplin/lib/locale,@joplin/lib/versionInfo— dropped the.defaultsinceimport X fromresolves the default export). - app/command-settingschema.ts, command-testing.ts, command-mkbook.ts, command-e2ee.ts — 1 each converted (
./base-command— TS file withexport default class BaseCommand). - app/command-apidoc.ts — 1 converted (
@joplin/lib/string-utils.js→@joplin/lib/string-utils). - app/cli-integration-tests.test.ts — 3 converted (
sqlite3,@joplin/lib/services/SettingUtils,./utils/shimInitCli); 2 left:@joplin/lib/database-driver-node.js(JS-only),@joplin/lib/shim-init-node.js(lib usesmodule.exports = {}). The typedshimInitClisurfaced a latent interface mismatch (ShimInitOptionsdeclaressharp,React,electronBridge,pdfJsas required, runtime defaults them to null); fixed in a follow-up by passing them asnullexplicitly. - app/gui/StatusBarWidget.ts — 2 converted (
chalk,strip-ansi); 3 left (tkwidgets/BaseWidget.js,tkwidgets/framework/termutils.js— no types,../autocompletion.js— JS). - app/LinkSelector.ts — 1 converted (
open).
Files skipped entirely (sibling command files use the CommonJS module.exports = Command pattern — converting requires changing the exported file too, which is out of scope):
- app/command-unpublish.test.ts, command-publish.test.ts, command-done.test.ts, command-rmbook.test.ts, command-rmnote.test.ts, command-share.test.ts, command-mkbook.test.ts —
require('./command-X')patterns.
Files skipped entirely (other reasons):
- app/command-sync.ts — 1 converted (
./base-command); the typedBaseCommandsurfaced a latent issue at line 65 (a variadiclog: (...s: string[])callback spread-passed to single-argstdout); fixed in the follow-up by narrowing the callback to single-arg. Other requires in the file (@joplin/lib/onedrive-api-node-utils.jsJS-only,./cli-utils.jsJS-only,md5no types) left. - app/command-keymap.ts, command-ls.ts, command-help.ts, command-import.ts, app/app.ts —
./cli-utils.jsand./help-utils.jsare JS-only. - app/app.ts —
@joplin/lib/Cacheis JS-only; line 164require(\./${path}`)and line 442require('./app-gui.js')` are dynamic/JS. - app/services/plugins/PluginRunner.ts, tests/services/plugins/sandboxProxy.ts —
@joplin/lib/services/plugins/sandboxProxyis JS-only. - app/utils/shimInitCli.ts —
@joplin/lib/shim-init-node.js(same lib issue as elsewhere). - app/gui/FolderListWidget.ts —
tkwidgets/ListWidget.js(no types). - app/command-edit.ts, command-e2ee.ts, command-ls.ts — inline
require()calls inside functions, or untyped (sprintf-js,md5,fs-extrainline). - tests/services/plugins/PluginService.ts — dynamic
require(contentScript.path).default.
packages/app-mobile
Session date: 2026-05-25
Files processed:
- PluginAssetsLoader.ts — 1 converted (
@joplin/lib/path-utils). - commands/util/showResource.ts — 1 converted (
react-native-file-viewer, default import). - components/NoteList.tsx — 1 converted (
@joplin/lib/locale). - components/NoteBodyViewer/hooks/useOnResourceLongPress.ts — 1 converted (
@joplin/lib/locale, dropped.jsextension). - components/SelectDateTimeDialog.tsx — 1 converted (
react-native-modal-datetime-picker, default). - components/side-menu-content.tsx — 1 converted (
@joplin/lib/string-utils). - components/screens/JoplinCloudLoginScreen.tsx — 2 converted (
react-redux,@joplin/lib/locale). - components/screens/Note/Note.tsx — 1 converted (
@react-native-clipboard/clipboard, default). - components/screens/UpgradeSyncTargetScreen.tsx — 1 converted (
react-redux). - root.tsx — 1 converted (
react-redux); severalrequire()s left for internal mobile.jsmodules (./components/app-nav.js,./components/screens/onedrive-login.js) and the@joplin/lib/SyncTarget*.jsfamily — TS counterparts either don't exist or coexist with.jsbuild artifacts in ways Metro currently relies on. - services/AlarmServiceDriver.ios.ts — 1 converted (
@react-native-community/push-notification-ios, default, merged with existingimport type). - utils/TlsUtils.ts, utils/setupNotifications.ts — 1 each converted (
react-nativenamed imports). - utils/buildStartupTasks.ts — 1 converted (
react-native-version-info, default); other requires left (SyncTarget JS-only,AlarmServiceDriverplatform extensions kept asrequireto avoid platform-resolution complications during this mechanical pass). - utils/fs-driver/fs-driver-rn.ts — 1 converted (
rn-fetch-blob, default);md5left (no types).
Files skipped entirely (latent issues surfaced; reverted for now, worth a follow-up):
- services/e2ee/RSA.react-native.ts — typed
react-native-rsa-nativereturnsKeyPair, but the file casts it to a localLegacyRsaKeyPairinterface with a missingkeySizeBitsproperty. Real interface drift; needs deciding whether the local interface should adapt or the cast is now wrong. - utils/database-driver-react-native.web.ts —
@sqlite.org/sqlite-wasmtypings only exportinit(default); the runtime exposessqlite3Worker1Promiseras a named property. Typed import fails — the.d.tsis incomplete vs the JS. Could be fixed with a localdeclare moduleaugmentation, but that's beyond a mechanical pass. - components/screens/dropbox-login.tsx — typed
connectfromreact-reduxinitially rejectedDropboxLoginScreenComponentbecauseBaseScreenComponentwas stillrequire()'d. Fixed in a follow-up: typed both imports, declaredProps/Stateinterfaces, and madestyles_/shared_proper instance fields.
Files skipped entirely (other reasons):
- utils/initReact.ts —
require('react')is deliberate (comment explains the timing constraint withshim.setReact). - utils/shim-init-react/index.web.ts —
: typeof ShimType = require()is the deliberate paired-with-import typeworkaround pattern. - utils/shim-init-react/index.ts — inline
require('react-native-version-info')and JS-onlygeolocation-react.js. - root.tsx —
react-native-dropdownalert(no types); see "Files processed" note for the internal mobile.jsrequires. - components/global-style.ts —
color(no types). - components/ProfileSwitcher/ProfileEditor.tsx —
react-native-paperis required asconst { TextInput } = require(...)but the package has no types. - components/screens/Notes/Notes.tsx, components/screens/Note/Note.tsx (line 85) —
@joplin/lib/reserved-idsis JS-only. - contentScripts/markdownEditorBundle/useWebViewSetup.ts —
@joplin/lib/resourceUtilsis JS-only. - contentScripts/richTextEditorBundle/contentScript/convertHtmlToMarkdown.ts —
@joplin/turndown,@joplin/turndown-plugin-gfm(no types). - services/voiceTyping/VoiceTyping.ts, fs-driver/fs-driver-rn.web.worker.ts, services/voiceTyping/whisper.test.ts —
md5/@joplin/whisper-voice-typingno types. - services/AlarmServiceDriver.android.ts —
@joplin/react-native-alarm-notification(no types). - utils/database-driver-react-native.ts —
react-native-sqlite-storage(no types). - utils/getPackageInfo.ts —
../packageInfo.js(JS-only). - components/plugins/backgroundPage/pluginRunnerBackgroundPage.ts —
pathandpunycode(mobile-specific bundling;punycode/has no types). - gulpfile.ts, web/webpack.config.ts — build-time files (
gulp,@joplin/tools/*,@pmmmwh/react-refresh-webpack-plugin,../babel.configare untyped or build-only).
Files skipped entirely:
- time.ts — 2 deliberate
const X: typeof X = require(...)workarounds for React Native bundler compatibility (already typed via pairedimport type). - ipc.ts —
tcp-port-usedhas no types. - cli.ts —
readline/promisesis not in the installed@types/node(16.x); migrating would require a node-types upgrade.
packages/app-desktop
Session date: 2026-05-25
This package has the highest concentration of cascading type errors when imports are typed: typed themeStyle / buildStyle from @joplin/lib/theme propagates strict CSSProperties union types (WhiteSpace, TextAlign, OverflowX, etc.) into inline style objects in many screens; typed connect from react-redux rejects components whose base class is still untyped (similar to the BaseScreenComponent case in app-mobile); typed styled-components and reselect.createSelector produce broad downstream errors. To keep this commit mechanical, those four patterns were intentionally not converted here. Many other safe patterns were converted.
Files processed:
- ElectronAppWrapper.ts — 1 converted (
@joplin/lib/path-utils); 3 left (@joplin/lib/shimtypeof-workaround,fs-extraredundant with existing import, inlineelectron-window-state). - InteropServiceHelper.ts — 1 converted (
@joplin/lib/path-utils). - commands/copyDevCommand.ts — 2 converted (
@electron/remote,electron.clipboard). - gui/ClipperConfigScreen.tsx — 1 converted (
electron.clipboard);connect,themeStyleleft (see note above). - gui/ErrorBoundary.tsx — 1 converted (
electron.ipcRenderer). - gui/MainScreen.tsx — 1 converted (
electron.ipcRenderer). - gui/MenuBar.tsx — 1 converted (
electron.clipboard). - gui/NoteEditor/NoteBody/CodeMirror/v5/CodeMirror.tsx — 1 converted (
electron.clipboard). - gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx — 1 converted (
electron.clipboard). - gui/NoteEditor/NoteEditor.tsx — 1 converted (
@joplin/lib/string-utils);themeStyleleft. - gui/NoteEditor/commands/pasteAsMarkdown.ts — 1 converted (
electron.clipboard). - gui/NoteEditor/utils/clipboardUtils.ts — 1 converted (
electron.clipboard). - gui/NoteEditor/utils/contextMenu.ts — 4 converted (
electron.clipboard,@joplin/lib/path-utils, twofs-extra). - gui/NoteEditor/utils/index.ts — 1 converted (
@joplin/rendererMarkupToHtml). - gui/NoteEditor/utils/resourceHandling.ts — 1 converted (
electron.clipboard);@joplin/renderer.utilsleft (the.utilsnamespace access doesn't cleanly map to a named import). - gui/NoteEditor/utils/useNoteSearchBar.ts — 1 converted (
@joplin/lib/services/CommandService, default). - gui/NoteListItem/utils/getNoteTitleHtml.ts — 1 converted (
@joplin/lib/string-utils). - gui/NotePropertiesDialog.tsx — 1 converted (
electron.clipboard). - gui/NoteRevisionViewer.tsx — 1 converted (
@joplin/lib/urlUtils);react-tooltipreverted because the typed import isn't usable as a JSX component (typings expose only the module namespace);connectleft. - gui/ResourceScreen.tsx — 1 converted (
pretty-bytes);connect,themeStyleleft. - gui/Root.tsx — 1 converted (
react-dom/client);connect, Provider,styled-componentsThemeProvider/StyleSheetManager/createGlobalStyleleft. - gui/Root_UpgradeSyncTarget.tsx — 1 converted (
electron.ipcRenderer). - gui/ShareNoteDialog.tsx — 1 converted (
electron.clipboard). - gui/WindowCommandsAndDialogs/commands/deleteFolder.ts — 1 converted (
@joplin/lib/string-utils). - gui/utils/NoteListUtils.ts — 1 converted (
electron.clipboard). - main-html.ts — 2 converted (
pdfjs-dist,is-apple-silicon);@joplin/lib/shim-init-node.js(lib'smodule.exports = {}issue) left. - plugins/GotoAnything.tsx — 1 converted (
electron.clipboard);connectleft. - services/plugins/PlatformImplementation.ts — 1 converted (
electron.clipboard, nativeImage). - services/plugins/PluginRunner.ts — 1 converted (
electron.ipcRenderer).
Follow-up session (2026-05-25): 5 more safe Node-built-in / typed-package conversions:
- ElectronAppWrapper.ts — 2 more converted (
path,fs-extra); consolidated with the existingimport { resolve } from 'path'soresolve(...)is nowpath.resolve(...). - InteropServiceHelper.ts — 1 more converted (
url). - gui/NoteEditor/utils/resourceHandling.ts — 1 more converted (
path). - gui/NoteEditor/utils/contextMenu.ts — 1 more converted (
fs-extra); folded the existingimport { writeFile } from 'fs-extra'into the namespace import.
Follow-up session (2026-05-25): full react-redux connect cluster — all 15 connect requires + 2 require('react') requires converted to typed imports. Zero react-redux/react require()s remain in app-desktop.
Files where the conversion was purely mechanical (Props already declared, or class was <any, any> with a matching (props: any) constructor):
- gui/HelpButton.tsx, gui/NoteStatusBar.tsx, gui/StatusScreen/StatusScreen.tsx, gui/ImportScreen.tsx, gui/ResourceScreen.tsx, gui/TagList.tsx, gui/NoteRevisionViewer.tsx, gui/JoplinCloudLoginScreen.tsx, plugins/GotoAnything.tsx, gui/ConfigScreen/ConfigScreen.tsx, gui/Root.tsx (just the
connect, Providerpart —styled-componentsleft).
Files that needed a small typing fix:
- gui/DropboxLoginScreen.tsx and gui/OneDriveLoginScreen.tsx — class was
<any, any>but the constructor took a narrowProps, so TS inferredPropsas the component's prop type. Typedconnectthen requiredPropsto match the connected/external shape. Fix: typed the class as<Props, State>with concreteStateinterfaces (Dropbox:{ loginUrl, authCode, checkingAuthToken }as written by the dropbox-login-shared.js helper; OneDrive:{ authLog: { key, text }[] }). Addeddispatch: Dispatch(fromredux) toProps(already used by the cancel-button handler). Dropbox'sProps.styleis typed{ width: number; height: number }to match whatNavigator.tsxpasses to its screens. - gui/TagItem.tsx — both
require('react')andrequire('react-redux'); the class extendedReact.Componentwith no props. Converted toimport * as React from 'react', declaredProps { themeId, title, id }(all already accessed onthis.props), and typed the class asReact.Component<Props>. - gui/ClipperConfigScreen.tsx — both
require('react')andrequire('react-redux'); the class extendedReact.Componentwith no props and the constructor calledsuper()with no args. Typed React needssuper(props). DeclaredProps { themeId, apiToken, clipperServer, clipperServerAutoStart }(all accessed inrender()), typed the class asReact.Component<Props>, and fixed the constructor.
Follow-up session (2026-05-25): full @joplin/lib/theme themeStyle / buildStyle cluster — all 9 converted. Seven files were purely mechanical: gui/hooks/useMarkupToHtml.ts, gui/KeymapConfig/styles/index.ts (merged with the existing import { ThemeStyle }), gui/NoteEditor/NoteBody/CodeMirror/Toolbar.tsx, gui/OneDriveLoginScreen.tsx, gui/TagList.tsx, gui/DropboxLoginScreen.tsx, gui/NoteEditor/NoteEditor.tsx. Two needed inline-style annotations to keep literal types from widening: gui/NoteContentPropertiesDialog.tsx (3 styles annotated React.CSSProperties for textAlign) and gui/ResourceScreen.tsx (3 styles annotated for whiteSpace/overflowX).
Follow-up session (2026-05-25): partial styled-components cluster (7 of 9 + reselect.createSelector).
Mechanical (no propagation):
- gui/Root.tsx (
ThemeProvider, StyleSheetManager, createGlobalStyle). - gui/NoteEditor/EditorWindow.tsx (
StyleSheetManager). - gui/Sidebar/styles/index.ts (
styled, merged with the existingimport { css }). - gui/style/StyledTextInput.tsx (
styled). - gui/ConfigScreen/ButtonBar.tsx (
styled). - gui/SearchBar/SearchBar.tsx (
styled). - gui/services/plugins/UserWebviewDialogButtonBar.tsx (
styled; the pairedstyled-systemspacekept asrequire()— no types installed). - gui/ExtensionBadge.tsx (
reselect.createSelector; split the selector's return into fourReact.CSSPropertiesconstants so the literal values forboxSizing/flexDirectionetc. stay narrow).
Still skipped:
- gui/style/StyledInput.tsx — typing it surfaces a latent bug in
PasswordInput.tsx(its customChangeEvent { value: string }shape doesn't match what the<input>actually fires) and a relatedFunction-typed handler chain inSearchInput.tsx. Reverted torequire()and flagged inreview-later.mdfor a separate fix.
Follow-up session (2026-05-25): typed styled-components in Button.tsx and adjacent fixes. With the underlying typing fixes below in place, every styled-components require() in app-desktop is now converted (9 / 9).
What changed in Button.tsx:
- Switched
ReactButtonPropsfromHTMLAttributes<HTMLButtonElement>toButtonHTMLAttributes<HTMLButtonElement>, which restores thetypeHTML attribute (<Button type='submit' />inSsoLoginScreen). - Declared a local
SpacePropsinterface (m/mt/mr/mb/ml/mx/my/p/pt/pr/pb/pl/px/py, all optional,number | string) and extendedPropswith it, so<Button mr=… ml=… mb=… />calls type-check.styled-systemhas no@types/*package and the migration explicitly avoids adding new types packages — the local interface documents the runtime-supported props. - Collapsed
StyledIconfromstyled(styled.span(space))tostyled.span<SpaceProps & StyleProps>\${space}; … `so the innermr` prop is in scope on the resulting component (runtime behaviour unchanged). - Fixed the pre-existing
${(props: StyleProps) => props.disabled}template interpolation inStyledButtonPrimary/StyledButtonSecondary. The original (introduced in commit67f0739d3, Nov 2020) interpolated abooleaninto the CSS template:false(most buttons most of the time) produced a bare{ … }block whose effect depended on the CSS parser's tolerance of malformed input;trueproduced an invalidtrue { … }rule that was dropped. Replaced with&:not(:disabled) {to express the obvious intent ("apply hover/active styles only when not disabled"), and removed the now-deaddisabled?: booleanfield fromStyleProps.
Knock-on fixes in callers (made necessary by the typed Button):
gui/ConfigScreen/ButtonBar.tsx— narrowedonCancelClick: Function/onSaveClick?: Function/onApplyClick?: Functionto()=> void(dropped three@typescript-eslint/ban-typesdisables); typed Button'sonClick: ()=> voidno longer accepts the broadFunction.gui/NoteListControls/NoteListControls.tsx— madeStyleProps.padding/StyleProps.buttonVerticalGapoptional. They were declared required on the sharedStylePropsbut only used byStyledRoot; with typedstyled(Button),<StyledPairButtonL>calls started failing because they (correctly) don't pass either.
styled-system space requires remain in Button.tsx, UserWebviewDialogButtonBar.tsx, and PluginsStates.tsx — the runtime ${space} template still needs the function, and the package has no types.
Files skipped entirely / important categories left untouched:
- gui/style/StyledInput.tsx — typing it surfaces a pre-existing
ChangeEvent { value: string }shape mismatch inPasswordInput.tsxand aFunction-typed handler chain inSearchInput.tsx. Flagged inreview-later.mdfor a separate fix. @joplin/lib/services/PluginManager(3),@joplin/lib/onedrive-api-node-utils.js(1),@joplin/lib/markJsUtils(1),@joplin/lib/countable/Countable(1),@joplin/lib/envFromArgs(1),@joplin/lib/components/shared/dropbox-login-shared(1),@joplin/lib/reserved-ids(2),./packageInfo.js(5),./services/electron-context-menu(1),./execCommand(1),./supportedLocales(1) — JS-only sources.@joplin/lib/shim-init-node.js(2) — samemodule.exports = { ... }issue described under packages/server.md5(4),debounce(5),color(2),styled-system(3),taboverride(1),source-map-support(1),react-toggle-button(1),formatcoords(1),gulp(1),@joplin/tools/*(3) — no types installed.- Inline
require()calls inside functions / arrow callbacks (bridge.ts, mockClipboard.ts, markdownEditor.spec.ts, ElectronAppWrapper.tselectron-window-state) — would require moving to top level.
packages/lib
Session date: 2026-05-25
Many conversions, but several modules surfaced latent type issues that need their own follow-up:
Categories converted (across roughly 40 files):
- Internal lib paths:
./path-utils,./string-utils,./urlUtils,./markdownUtils,./errorUtils,./locale(when paired with a TS source). - Node built-ins:
path,os,url,events(where it doesn't cascade),https,http,crypto,buffer,zlib,timers,dgram,querystring— namespace or destructured. - npm packages:
async-mutex(incl. the.Mutexaccess pattern),html-entities(.AllHtmlEntities),chokidar,fast-deep-equal,query-string,markdown-it,md5-file,string-to-stream,@joplin/renderer(MarkupToHtml),sqlite3.
Latent issues surfaced & reverted (worth follow-ups):
momentintime.ts: typedmoment()rejects thestring | number | Date | { toDate(): Date }union —anythingToDateTime/anythingToMsrely on narrowingopast{ toDate(): Date }via'toDate' in o, but TS doesn't propagate the narrowing into themoment(o, format)call. Fixed in a follow-up by converting to a typed namespace import and adding aconst input = o as string | number | Datecast for the moment calls.services/interop/InteropService_Importer_Md_frontmatter.test.tskeepsrequire('moment')for now.fs-extrainfs-driver-node.ts: typedfs.appendFile(path, content, { encoding })andfs.writeFile(path, content, { encoding })don't accept{ encoding: string }— they expect{ encoding: BufferEncoding }. Fixed in a follow-up by converting therequire()to a typed namespace import and castingencoding as BufferEncodingat the three internal fs call sites; the public method signatures stay asstringto match the base class.reselect.createSelectorCreator/defaultMemoizeandre-reselect.createCachedSelector(inreducer.ts,services/commands/MenuUtils.ts,services/commands/ToolbarButtonUtils.ts): typedcreateCachedSelectorreturns a different signature than the codebase assumes —selectArrayShallow(props, cacheKey)then errors withExpected 1 arguments, but got 2. Probably the reselect version was upgraded without updating call sites.events.EventEmitterinservices/plugins/Plugin.ts: typedEventEmitter.on(event, fn)rejectsFunction-typed callbacks. Fixed in a follow-up: tighten the two callbacks to(...args: unknown[]) => void, switch to a typedimport { EventEmitter } from 'events', drop the two@typescript-eslint/ban-typesdisables and theInstanceType<typeof EventEmitter>workaround.@joplin/lib/string-utilsformatCssSizeinservices/style/themeToCss.ts: same latent issue as the desktop'suseThemeCss.ts—formatCssSize(value: string)was called withnumber. Fixed in a follow-up by widening the signature tostring | number(the body already handled both at runtime); both call sites then convert cleanly to typed imports.
Files skipped entirely (other reasons):
@joplin/lib/shim-init-node(multiple places) and JS-only sibling files (./reserved-ids,./database-driver-node.js,./file-api.js,./file-api-driver-onedrive.js,./file-api-driver-webdav,./randomClipperPort,./import-enex-md-gen.js,./import-enex-html-gen.js,./resourceUtils.js,./SyncTarget*.js,./parameters.js,./vendor/*,./welcomeAssets) — JS-only, no TS counterpart.- No-types npm packages:
sprintf-js(14),md5(9),url-parse(3),tcp-port-used(2),string-padding(2),@joplin/fork-sax(2),word-wrap,uglifycss,server-destroy,relative,node-notifier,multiparty,image-data-uri,hpagent,diff-match-patch,base64-stream,base-64,@joplin/turndown,@joplin/turndown-plugin-gfm,@aws-sdk/client-s3,@adobe/css-tools,better-sqlite3,sharp,color,@joplin/fork-uslug. - Inline
require()inside functions /.defaultaccesses on JS-only modules (shim-init-node.tshas many of these insideshimInit). electronrequires insideshim-init-node.ts(lib used in node + electron; therequire('electron').nativeImagecalls inside conditional branches are deliberately late-loaded).EventDispatcher.test.tsline 125: kept asrequire(...).defaultbecause the test specifically asserts that therequire + .defaultpattern works.