diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json
index 3c331da7c21960d69757d7dde8c496e436ddec8d..952c5dcc18264a4d2ccda4fa93e5a5ed5f0262dc 100644
--- a/packages/desktop-client/package.json
+++ b/packages/desktop-client/package.json
@@ -34,7 +34,7 @@
     "inter-ui": "^3.19.3",
     "jest": "^27.0.0",
     "jest-watch-typeahead": "^2.2.2",
-    "memoize-one": "^4.0.0",
+    "memoize-one": "^6.0.0",
     "pikaday": "1.8.0",
     "react": "18.2.0",
     "react-app-rewired": "^2.2.1",
diff --git a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx
index b4b1e6b5d0cb0def828bd806b033dd9a63bd0797..15afe636f192258281beac1e8bf304bf4bf82a98 100644
--- a/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx
+++ b/packages/desktop-client/src/components/budget/rollover/BudgetSummary.tsx
@@ -256,7 +256,7 @@ function ToBudget({
 }
 
 type BudgetSummaryProps = {
-  month: string | number;
+  month: string;
   isGoalTemplatesEnabled: boolean;
 };
 export function BudgetSummary({
diff --git a/packages/loot-core/package.json b/packages/loot-core/package.json
index 2abe13dc74232c511cdd2bffedf89cf0178dffb1..14a1a1616149ad87c7ebb0ba66282135b35ff777 100644
--- a/packages/loot-core/package.json
+++ b/packages/loot-core/package.json
@@ -63,7 +63,7 @@
     "jsverify": "^0.8.4",
     "lru-cache": "^5.1.1",
     "memfs": "3.1.1",
-    "memoize-one": "^4.0.0",
+    "memoize-one": "^6.0.0",
     "mockdate": "^3.0.5",
     "npm-run-all": "^4.1.3",
     "peggy": "3.0.2",
diff --git a/packages/loot-core/src/client/actions/account.ts b/packages/loot-core/src/client/actions/account.ts
index 6d9ef4350fa00b5bae06cc0ba79e78f7512d6708..e793d4936040037444be997dde53b6aa748cc308 100644
--- a/packages/loot-core/src/client/actions/account.ts
+++ b/packages/loot-core/src/client/actions/account.ts
@@ -198,7 +198,6 @@ export function importTransactions(id, transactions) {
         addNotification({
           type: 'error',
           message: error.message,
-          internal: error.internal,
         }),
       );
     });
diff --git a/packages/loot-core/src/client/actions/app.ts b/packages/loot-core/src/client/actions/app.ts
index 1230fc76d38211236b41eea76f084fb9acb5d59a..7ee582794a33b8a97567b4d276297155dcdf1664 100644
--- a/packages/loot-core/src/client/actions/app.ts
+++ b/packages/loot-core/src/client/actions/app.ts
@@ -25,7 +25,7 @@ export function setLastUndoState(undoState) {
 // This is only used in the fake web version where everything runs in
 // the browser. It's a way to send a file to the backend to be
 // imported into the virtual filesystem.
-export function uploadFile(filename, contents) {
+export function uploadFile(filename: string, contents: ArrayBuffer) {
   return dispatch => {
     return send('upload-file-web', { filename, contents });
   };
diff --git a/packages/loot-core/src/client/actions/sync.ts b/packages/loot-core/src/client/actions/sync.ts
index 81ec013d32e86cdae359d62a0f99d10a046d84ee..bb5682cb6d8565e4da247efb17d489ee5a5f27f4 100644
--- a/packages/loot-core/src/client/actions/sync.ts
+++ b/packages/loot-core/src/client/actions/sync.ts
@@ -13,7 +13,8 @@ export function resetSync() {
       alert(getUploadError(error));
 
       if (
-        (error.reason === 'encrypt-failure' && error.meta.isMissingKey) ||
+        (error.reason === 'encrypt-failure' &&
+          (error.meta as { isMissingKey?: boolean }).isMissingKey) ||
         error.reason === 'file-has-new-key'
       ) {
         dispatch(
@@ -38,8 +39,10 @@ export function sync() {
   return async (dispatch, getState) => {
     const prefs = getState().prefs.local;
     if (prefs && prefs.id) {
-      let { error } = await send('sync');
-      return { error };
+      let result = await send('sync');
+      if ('error' in result) {
+        return { error: result.error };
+      }
     }
   };
 }
diff --git a/packages/loot-core/src/mocks/budget.ts b/packages/loot-core/src/mocks/budget.ts
index ba559dc747d0372ce93add8f917022802e965034..56f7c894df29aeda33efa65728a57084d7f53a84 100644
--- a/packages/loot-core/src/mocks/budget.ts
+++ b/packages/loot-core/src/mocks/budget.ts
@@ -141,7 +141,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
   );
   let currentDay = monthUtils.currentDay();
   for (let month of months) {
-    let date = monthUtils.addDays(month, '12');
+    let date = monthUtils.addDays(month, 12);
     if (monthUtils.isBefore(date, currentDay)) {
       transactions.push({
         amount: -10000,
@@ -152,7 +152,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
       });
     }
 
-    date = monthUtils.addDays(month, '18');
+    date = monthUtils.addDays(month, 18);
     if (monthUtils.isBefore(date, currentDay)) {
       transactions.push({
         amount: -9000,
@@ -163,7 +163,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
       });
     }
 
-    date = monthUtils.addDays(month, '2');
+    date = monthUtils.addDays(month, 2);
     if (monthUtils.isBefore(date, currentDay)) {
       transactions.push({
         amount: -120000,
@@ -174,7 +174,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
       });
     }
 
-    date = monthUtils.addDays(month, '20');
+    date = monthUtils.addDays(month, 20);
     if (monthUtils.isBefore(date, currentDay)) {
       transactions.push({
         amount: -6000,
@@ -186,7 +186,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
       });
     }
 
-    date = monthUtils.addDays(month, '23');
+    date = monthUtils.addDays(month, 23);
     if (monthUtils.isBefore(date, currentDay)) {
       transactions.push({
         amount: -7500,
diff --git a/packages/loot-core/src/platform/server/connection/index.d.ts b/packages/loot-core/src/platform/server/connection/index.d.ts
index d9c8b557db0de3958f7f9ba53d25b88a99007c82..419e972626458303e54a538e2b8e0f0f324ea4d4 100644
--- a/packages/loot-core/src/platform/server/connection/index.d.ts
+++ b/packages/loot-core/src/platform/server/connection/index.d.ts
@@ -1,8 +1,9 @@
+import type { Handlers } from '../../../types/handlers';
 import type { ServerEvents } from '../../../types/server-events';
 
 export function init(
   channel: Window | number, // in electron the port number, in web the worker
-  handlers: Record<string, () => void>,
+  handlers: Handlers,
 ): void;
 export type Init = typeof init;
 
diff --git a/packages/loot-core/src/server/accounts/parse-file.test.ts b/packages/loot-core/src/server/accounts/parse-file.test.ts
index b420762138f4ba9387c82b6430d7cd8b2527827d..0c0c9a16a923d1815a1b429501176f4d126148d3 100644
--- a/packages/loot-core/src/server/accounts/parse-file.test.ts
+++ b/packages/loot-core/src/server/accounts/parse-file.test.ts
@@ -39,7 +39,8 @@ async function importFileWithRealTime(
   global.restoreFakeDateNow();
 
   if (transactions) {
-    transactions = transactions.map(trans => ({
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    transactions = (transactions as any[]).map(trans => ({
       ...trans,
       amount: amountToInteger(trans.amount),
       date: dateFormat
diff --git a/packages/loot-core/src/server/accounts/parse-file.ts b/packages/loot-core/src/server/accounts/parse-file.ts
index 8819629fb04c4b31a00d36c38281e1570d4f7fc0..2922e1d2ebcb7e2b82f82da8896f953639260166 100644
--- a/packages/loot-core/src/server/accounts/parse-file.ts
+++ b/packages/loot-core/src/server/accounts/parse-file.ts
@@ -6,8 +6,17 @@ import { looselyParseAmount } from '../../shared/util';
 
 import qif2json from './qif2json';
 
-export function parseFile(filepath, options?: unknown) {
-  let errors = [];
+type ParseError = { message: string; internal: string };
+export type ParseFileResult = {
+  errors?: ParseError[];
+  transactions?: unknown[];
+};
+
+export async function parseFile(
+  filepath,
+  options?: unknown,
+): Promise<ParseFileResult> {
+  let errors = Array<ParseError>();
   let m = filepath.match(/\.[^.]*$/);
 
   if (m) {
@@ -32,8 +41,11 @@ export function parseFile(filepath, options?: unknown) {
   return { errors, transactions: undefined };
 }
 
-async function parseCSV(filepath, options: { delimiter?: string } = {}) {
-  let errors = [];
+async function parseCSV(
+  filepath,
+  options: { delimiter?: string } = {},
+): Promise<ParseFileResult> {
+  let errors = Array<ParseError>();
   let contents = await fs.readFile(filepath);
 
   let data;
@@ -59,8 +71,8 @@ async function parseCSV(filepath, options: { delimiter?: string } = {}) {
   return { errors, transactions: data };
 }
 
-async function parseQIF(filepath) {
-  let errors = [];
+async function parseQIF(filepath): Promise<ParseFileResult> {
+  let errors = Array<ParseError>();
   let contents = await fs.readFile(filepath);
 
   let data;
@@ -75,7 +87,7 @@ async function parseQIF(filepath) {
   }
 
   return {
-    errors,
+    errors: [],
     transactions: data.transactions.map(trans => ({
       amount: trans.amount != null ? looselyParseAmount(trans.amount) : null,
       date: trans.date,
@@ -86,13 +98,13 @@ async function parseQIF(filepath) {
   };
 }
 
-async function parseOFX(filepath) {
+async function parseOFX(filepath): Promise<ParseFileResult> {
   let { getOFXTransactions, initModule } = await import(
     /* webpackChunkName: 'xfo' */ 'node-libofx'
   );
   await initModule();
 
-  let errors = [];
+  let errors = Array<ParseError>();
   let contents = await fs.readFile(filepath, 'binary');
 
   let data;
diff --git a/packages/loot-core/src/server/accounts/transactions.ts b/packages/loot-core/src/server/accounts/transactions.ts
index 2775825eb0ad7c97424be5983ea2ef0c1be233c0..c57b29724302023bc49af6c3c8c36052b0650c5b 100644
--- a/packages/loot-core/src/server/accounts/transactions.ts
+++ b/packages/loot-core/src/server/accounts/transactions.ts
@@ -1,4 +1,5 @@
 import * as connection from '../../platform/server/connection';
+import { TransactionEntity } from '../../types/models';
 import * as db from '../db';
 import { incrFetch, whereIn } from '../db/util';
 import { batchMessages } from '../sync';
@@ -18,7 +19,9 @@ async function idsWithChildren(ids: string[]) {
   return [...set];
 }
 
-async function getTransactionsByIds(ids: string[]) {
+async function getTransactionsByIds(
+  ids: string[],
+): Promise<TransactionEntity[]> {
   // TODO: convert to whereIn
   //
   // or better yet, use ActualQL
@@ -42,13 +45,13 @@ export async function batchUpdateTransactions({
   deleted?: Array<{ id: string; payee: unknown }>;
   updated?: Array<{
     id: string;
-    payee: unknown;
-    account: unknown;
-    category: unknown;
+    payee?: unknown;
+    account?: unknown;
+    category?: unknown;
   }>;
   learnCategories?: boolean;
   detectOrphanPayees?: boolean;
-}) {
+}): Promise<{ added: TransactionEntity[]; updated: unknown[] }> {
   // Track the ids of each type of transaction change (see below for why)
   let addedIds = [];
   let updatedIds = updated ? updated.map(u => u.id) : [];
@@ -123,7 +126,7 @@ export async function batchUpdateTransactions({
   // to the client so that can apply them. Note that added
   // transactions just return the full transaction.
   let resultAdded = allAdded;
-  let resultUpdated;
+  let resultUpdated: unknown[];
 
   await batchMessages(async () => {
     await Promise.all(allAdded.map(t => transfer.onInsert(t)));
diff --git a/packages/loot-core/src/server/api.ts b/packages/loot-core/src/server/api.ts
index 85ad5183bf817a56c0aa99ba9292ff99ee1bff65..2d67d5e9c0655a639b81279bf82929c3e69d190e 100644
--- a/packages/loot-core/src/server/api.ts
+++ b/packages/loot-core/src/server/api.ts
@@ -14,6 +14,8 @@ import {
   deleteTransaction,
 } from '../shared/transactions';
 import { integerToAmount } from '../shared/util';
+import { Handlers } from '../types/handlers';
+import { ServerHandlers } from '../types/server-handlers';
 
 import { addTransactions } from './accounts/sync';
 import {
@@ -68,7 +70,7 @@ function withMutation(handler) {
   };
 }
 
-let handlers = {};
+let handlers = {} as unknown as Handlers;
 
 async function validateMonth(month) {
   if (!month.match(/^\d{4}-\d{2}$/)) {
@@ -593,7 +595,8 @@ handlers['api/payee-delete'] = withMutation(async function ({ id }) {
   return handlers['payees-batch-change']({ deleted: [{ id }] });
 });
 
-export default function installAPI(serverHandlers) {
-  handlers = Object.assign({}, serverHandlers, handlers);
-  return handlers;
+export default function installAPI(serverHandlers: ServerHandlers) {
+  let merged = Object.assign({}, serverHandlers, handlers);
+  handlers = merged as Handlers;
+  return merged;
 }
diff --git a/packages/loot-core/src/server/app.ts b/packages/loot-core/src/server/app.ts
index c2bca043a6fbf6e8d1a13f134458c3b3e06d9ca2..e52ffa2940a36c4c4f26e448b6fbf2ccdea94ed2 100644
--- a/packages/loot-core/src/server/app.ts
+++ b/packages/loot-core/src/server/app.ts
@@ -7,20 +7,23 @@ import { captureException } from '../platform/exceptions';
 // makes it cleaner to combine methods. We call a group of related
 // methods an "app".
 
-class App {
+class App<Handlers> {
   events;
-  handlers;
+  handlers: Handlers;
   services;
   unlistenServices;
 
   constructor() {
-    this.handlers = {};
+    this.handlers = {} as Handlers;
     this.services = [];
     this.events = mitt();
     this.unlistenServices = [];
   }
 
-  method(name, func) {
+  method<Name extends string & keyof Handlers>(
+    name: Name,
+    func: Handlers[Name],
+  ) {
     if (this.handlers[name] != null) {
       throw new Error(
         'Conflicting method name, names must be globally unique: ' + name,
@@ -36,7 +39,7 @@ class App {
   combine(...apps) {
     for (let app of apps) {
       Object.keys(app.handlers).forEach(name => {
-        this.method(name, app.handlers[name]);
+        this.method(name as string & keyof Handlers, app.handlers[name]);
       });
 
       app.services.forEach(service => {
@@ -72,6 +75,6 @@ class App {
   }
 }
 
-export function createApp() {
-  return new App();
+export function createApp<T>() {
+  return new App<T>();
 }
diff --git a/packages/loot-core/src/server/backups.ts b/packages/loot-core/src/server/backups.ts
index 2919b66df7caa070bb02afe0ec6b6f5f3ac08f90..69afbd4c7deaa8a7acfb6ba4f72f51b4273575c0 100644
--- a/packages/loot-core/src/server/backups.ts
+++ b/packages/loot-core/src/server/backups.ts
@@ -14,7 +14,11 @@ import * as prefs from './prefs';
 const LATEST_BACKUP_FILENAME = 'db.latest.sqlite';
 let serviceInterval = null;
 
-async function getBackups(id) {
+export type Backup = { id: string; date: string } | LatestBackup;
+type LatestBackup = { id: string; date: null; isLatest: true };
+type BackupWithDate = { id: string; date: Date };
+
+async function getBackups(id: string): Promise<BackupWithDate[]> {
   const budgetDir = fs.getBudgetDir(id);
   const backupDir = fs.join(budgetDir, 'backups');
 
@@ -46,7 +50,7 @@ async function getBackups(id) {
   return backups;
 }
 
-async function getLatestBackup(id) {
+async function getLatestBackup(id: string): Promise<LatestBackup | null> {
   const budgetDir = fs.getBudgetDir(id);
   if (await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME))) {
     return {
@@ -58,7 +62,7 @@ async function getLatestBackup(id) {
   return null;
 }
 
-export async function getAvailableBackups(id) {
+export async function getAvailableBackups(id: string): Promise<Backup[]> {
   let backups = await getBackups(id);
 
   let latestBackup = await getLatestBackup(id);
@@ -97,7 +101,7 @@ export async function updateBackups(backups) {
   return removed.concat(currentBackups.slice(10).map(backup => backup.id));
 }
 
-export async function makeBackup(id) {
+export async function makeBackup(id: string) {
   const budgetDir = fs.getBudgetDir(id);
 
   // When making a backup, we no longer consider the user to be
@@ -130,7 +134,7 @@ export async function makeBackup(id) {
   connection.send('backups-updated', await getAvailableBackups(id));
 }
 
-export async function loadBackup(id, backupId) {
+export async function loadBackup(id: string, backupId: string) {
   const budgetDir = fs.getBudgetDir(id);
 
   if (!(await fs.exists(fs.join(budgetDir, LATEST_BACKUP_FILENAME)))) {
@@ -203,7 +207,7 @@ export async function loadBackup(id, backupId) {
   }
 }
 
-export function startBackupService(id) {
+export function startBackupService(id: string) {
   if (serviceInterval) {
     clearInterval(serviceInterval);
   }
diff --git a/packages/loot-core/src/server/budget/app.ts b/packages/loot-core/src/server/budget/app.ts
index 6360e2a6d0bfc6c4f8e9373765aa39c27bd36f5e..db683c5df9ab646da89e398098f38485610c2115 100644
--- a/packages/loot-core/src/server/budget/app.ts
+++ b/packages/loot-core/src/server/budget/app.ts
@@ -5,8 +5,9 @@ import { undoable } from '../undo';
 import * as actions from './actions';
 import * as cleanupActions from './cleanup-template';
 import * as goalActions from './goaltemplates';
+import { BudgetHandlers } from './types/handlers';
 
-let app = createApp();
+let app = createApp<BudgetHandlers>();
 
 app.method('budget/budget-amount', mutator(undoable(actions.setBudget)));
 app.method(
diff --git a/packages/loot-core/src/server/budget/base.ts b/packages/loot-core/src/server/budget/base.ts
index 3489be3f22817d6790d45b1b0f65f6daaa0dc30e..6e4ba5b4f879246b86f246d0b3b844576d50a429 100644
--- a/packages/loot-core/src/server/budget/base.ts
+++ b/packages/loot-core/src/server/budget/base.ts
@@ -14,7 +14,7 @@ export function getBudgetType() {
   return meta.budgetType || 'rollover';
 }
 
-export function getBudgetRange(start, end) {
+export function getBudgetRange(start: string, end: string) {
   start = monthUtils.getMonth(start);
   end = monthUtils.getMonth(end);
 
diff --git a/packages/loot-core/src/server/budget/types/handlers.d.ts b/packages/loot-core/src/server/budget/types/handlers.d.ts
index bca907c64b07f9f47e361870b0b6522b83ce1923..d779784048ecd028c250f40b900cabc2ca128224 100644
--- a/packages/loot-core/src/server/budget/types/handlers.d.ts
+++ b/packages/loot-core/src/server/budget/types/handlers.d.ts
@@ -1,5 +1,9 @@
 export interface BudgetHandlers {
-  'budget/budget-amount': (...args: unknown[]) => Promise<unknown>;
+  'budget/budget-amount': (arg: {
+    category: string /* category id */;
+    month: string;
+    amount: number;
+  }) => Promise<unknown>;
 
   'budget/copy-previous-month': (...args: unknown[]) => Promise<unknown>;
 
diff --git a/packages/loot-core/src/server/cloud-storage.ts b/packages/loot-core/src/server/cloud-storage.ts
index f6829c1bc86d0b0499c97fc851042a1c141cab4a..7d61118b5e91d7303a9b84465436983efe654e2c 100644
--- a/packages/loot-core/src/server/cloud-storage.ts
+++ b/packages/loot-core/src/server/cloud-storage.ts
@@ -21,6 +21,15 @@ import { getServer } from './server-config';
 
 let UPLOAD_FREQUENCY_IN_DAYS = 7;
 
+export interface RemoteFile {
+  deleted: boolean;
+  fileId: string;
+  groupId: string;
+  name: string;
+  encryptKeyId: string;
+  hasKey: boolean;
+}
+
 async function checkHTTPStatus(res) {
   if (res.status !== 200) {
     return res.text().then(str => {
@@ -37,7 +46,10 @@ async function fetchJSON(...args: Parameters<typeof fetch>) {
   return res.json();
 }
 
-export async function checkKey() {
+export async function checkKey(): Promise<{
+  valid: boolean;
+  error?: { reason: string };
+}> {
   let userToken = await asyncStorage.getItem('user-token');
 
   let { cloudFileId, encryptKeyId } = prefs.getPrefs();
@@ -50,7 +62,7 @@ export async function checkKey() {
     });
   } catch (e) {
     console.log(e);
-    return { error: { reason: 'network' } };
+    return { valid: false, error: { reason: 'network' } };
   }
 
   return {
@@ -325,7 +337,7 @@ export async function removeFile(fileId) {
   });
 }
 
-export async function listRemoteFiles() {
+export async function listRemoteFiles(): Promise<RemoteFile[] | null> {
   let userToken = await asyncStorage.getItem('user-token');
   if (!userToken) {
     return null;
diff --git a/packages/loot-core/src/server/main-app.ts b/packages/loot-core/src/server/main-app.ts
index 6e89fc1ef9edb79135fc020b745d9a2d6fc64f73..e884b2332ded29e13d914dce8f67ba4f4e89333f 100644
--- a/packages/loot-core/src/server/main-app.ts
+++ b/packages/loot-core/src/server/main-app.ts
@@ -1,9 +1,10 @@
 import * as connection from '../platform/server/connection';
+import { Handlers } from '../types/handlers';
 
 import { createApp } from './app';
 
 // Main app
-const app = createApp();
+const app = createApp<Handlers>();
 
 app.events.on('sync', info => {
   connection.send('sync-event', info);
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index c6553629f5394534f7e65e11612e2383ff8e4f13..1cf496e8e8af12cf1a1d37c8f3aeccbd38917247 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -17,6 +17,7 @@ import * as monthUtils from '../shared/months';
 import q, { Query } from '../shared/query';
 import { FIELD_TYPES as ruleFieldTypes } from '../shared/rules';
 import { amountToInteger, stringToInteger } from '../shared/util';
+import { Handlers } from '../types/handlers';
 
 import { exportToCSV, exportQueryToCSV } from './accounts/export-to-csv';
 import * as link from './accounts/link';
@@ -84,7 +85,9 @@ function onSheetChange({ names }) {
 
 // handlers
 
-export let handlers = {};
+// need to work around the type system here because the object
+// is /currently/ empty but we promise to fill it in later
+export let handlers = {} as unknown as Handlers;
 
 handlers['undo'] = mutator(async function () {
   return undo();
@@ -2264,7 +2267,7 @@ handlers['upload-file-web'] = async function ({ filename, contents }) {
   }
 
   await fs.writeFile('/uploads/' + filename, contents);
-  return 'ok';
+  return {};
 };
 
 handlers['backups-get'] = async function ({ id }) {
@@ -2300,7 +2303,7 @@ handlers['app-focused'] = async function () {
   }
 };
 
-handlers = installAPI(handlers);
+handlers = installAPI(handlers) as Handlers;
 
 injectAPI.override((name, args) => runHandler(app.handlers[name], args));
 
diff --git a/packages/loot-core/src/server/mutators.ts b/packages/loot-core/src/server/mutators.ts
index bd5035084c7d999788fbd68193a4fc56240d5153..c23900b6629c7100aa63e6c42e7129b9a8c7ee9e 100644
--- a/packages/loot-core/src/server/mutators.ts
+++ b/packages/loot-core/src/server/mutators.ts
@@ -9,7 +9,9 @@ let globalMutationsEnabled = false;
 
 let _latestHandlerNames = [];
 
-export function mutator(handler) {
+export function mutator<T extends (...args: unknown[]) => unknown>(
+  handler: T,
+): T {
   mutatingMethods.set(handler, true);
   return handler;
 }
@@ -93,7 +95,10 @@ function _runMutator<T extends () => Promise<unknown>>(
 // Type cast needed as TS looses types over nested generic returns
 export const runMutator = sequential(_runMutator) as typeof _runMutator;
 
-export function withMutatorContext(context, func) {
+export function withMutatorContext<T>(
+  context: { undoListening: boolean; undoTag?: unknown },
+  func: () => Promise<T>,
+): Promise<T> {
   if (currentContext == null && !globalMutationsEnabled) {
     captureBreadcrumb('Recent methods: ' + _latestHandlerNames.join(', '));
     captureException(new Error('withMutatorContext: mutator not running'));
diff --git a/packages/loot-core/src/server/notes/app.ts b/packages/loot-core/src/server/notes/app.ts
index ba6c9f7ca6d862e21f60fd3fc44b565c71d766ac..bc76a5be192a809e24ebb508f23c2c5629ff0f71 100644
--- a/packages/loot-core/src/server/notes/app.ts
+++ b/packages/loot-core/src/server/notes/app.ts
@@ -1,7 +1,9 @@
 import { createApp } from '../app';
 import * as db from '../db';
 
-let app = createApp();
+import { NotesHandlers } from './types/handlers';
+
+let app = createApp<NotesHandlers>();
 
 app.method('notes-save', async ({ id, note }) => {
   await db.update('notes', { id, note });
diff --git a/packages/loot-core/src/server/schedules/app.ts b/packages/loot-core/src/server/schedules/app.ts
index c2f3cfe07759c4a82df3bd098bcbdbcd5fdc9a6f..fded914635c593f8f7415eb301e69f5f40a2364e 100644
--- a/packages/loot-core/src/server/schedules/app.ts
+++ b/packages/loot-core/src/server/schedules/app.ts
@@ -32,6 +32,7 @@ import { undoable } from '../undo';
 import { Schedule as RSchedule } from '../util/rschedule';
 
 import { findSchedules } from './find-schedules';
+import { SchedulesHandlers } from './types/handlers';
 
 // Utilities
 
@@ -357,11 +358,6 @@ async function skipNextDate({ id }) {
     },
   });
 }
-
-// `schedule` here might not be a saved schedule, so it might not have
-// an id
-function getPossibleTransactions({ schedule }) {}
-
 function discoverSchedules() {
   return findSchedules();
 }
@@ -437,7 +433,7 @@ function onApplySync(oldValues, newValues) {
 // This is the service that move schedules forward automatically and
 // posts transactions
 
-async function postTransactionForSchedule({ id }) {
+async function postTransactionForSchedule({ id }: { id: string }) {
   let { data } = await aqlQuery(q('schedules').filter({ id }).select('*'));
   let schedule = data[0];
   if (schedule == null || schedule._account == null) {
@@ -533,7 +529,7 @@ async function advanceSchedulesService(syncSuccess) {
 }
 
 // Expose functions to the client
-let app = createApp();
+let app = createApp<SchedulesHandlers>();
 
 app.method('schedule/create', mutator(undoable(createSchedule)));
 app.method('schedule/update', mutator(undoable(updateSchedule)));
@@ -547,7 +543,6 @@ app.method(
   'schedule/force-run-service',
   mutator(() => advanceSchedulesService(true)),
 );
-app.method('schedule/get-possible-transactions', getPossibleTransactions);
 app.method('schedule/discover', discoverSchedules);
 app.method('schedule/get-upcoming-dates', getUpcomingDates);
 
diff --git a/packages/loot-core/src/server/schedules/types/handlers.ts b/packages/loot-core/src/server/schedules/types/handlers.ts
index 7f7ec60d6f7a4857fb7fb07083cf942e5000f610..0992b858aee8d6589e3a0bb65047e8b043821363 100644
--- a/packages/loot-core/src/server/schedules/types/handlers.ts
+++ b/packages/loot-core/src/server/schedules/types/handlers.ts
@@ -1,19 +1,27 @@
 export interface SchedulesHandlers {
-  'schedule/create': () => Promise<unknown>;
+  'schedule/create': (arg: {
+    schedule: unknown;
+    conditions: unknown[];
+  }) => Promise<string>;
 
-  'schedule/update': () => Promise<unknown>;
+  'schedule/update': (schedule: {
+    schedule;
+    conditions?;
+    resetNextDate?: boolean;
+  }) => Promise<void>;
 
-  'schedule/delete': () => Promise<unknown>;
+  'schedule/delete': (arg: { id: string }) => Promise<void>;
 
-  'schedule/skip-next-date': () => Promise<unknown>;
+  'schedule/skip-next-date': (arg: { id: string }) => Promise<void>;
 
-  'schedule/post-transaction': () => Promise<unknown>;
+  'schedule/post-transaction': (arg: { id: string }) => Promise<void>;
 
   'schedule/force-run-service': () => Promise<unknown>;
 
-  'schedule/get-possible-transactions': () => Promise<unknown>;
-
   'schedule/discover': () => Promise<unknown>;
 
-  'schedule/get-upcoming-dates': () => Promise<unknown>;
+  'schedule/get-upcoming-dates': (arg: {
+    config;
+    count: number;
+  }) => Promise<string[]>;
 }
diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts
index e41ed60880d468c3573195a93d449c7ec8b0fbca..d2ee55476d3b92616a35bca7b596268258bc603c 100644
--- a/packages/loot-core/src/server/sync/index.ts
+++ b/packages/loot-core/src/server/sync/index.ts
@@ -562,7 +562,8 @@ export async function initialFullSync(): Promise<void> {
 }
 
 export const fullSync = once(async function (): Promise<
-  { messages: Message[] } | { error: unknown }
+  | { messages: Message[] }
+  | { error: { message: string; reason: string; meta: unknown } }
 > {
   app.events.emit('sync', { type: 'start' });
   let messages;
diff --git a/packages/loot-core/src/server/sync/reset.ts b/packages/loot-core/src/server/sync/reset.ts
index 27e18feb92ce1801c6145ace8a601b21ac46ac39..ae8627757f47cf00f1475beb9d8a6b961eb9e6f3 100644
--- a/packages/loot-core/src/server/sync/reset.ts
+++ b/packages/loot-core/src/server/sync/reset.ts
@@ -6,7 +6,9 @@ import * as db from '../db';
 import { runMutator } from '../mutators';
 import * as prefs from '../prefs';
 
-export default async function resetSync(keyState?) {
+export default async function resetSync(
+  keyState?,
+): Promise<{ error?: { reason: string; meta?: unknown } }> {
   if (!keyState) {
     // If we aren't resetting the key, make sure our key is up-to-date
     // so we don't accidentally upload a file encrypted with the wrong
diff --git a/packages/loot-core/src/server/tools/app.ts b/packages/loot-core/src/server/tools/app.ts
index 36d65470eabef26227e06ff7a2c13845e26f2695..867ad2638090fe415cfa03257bc2eedb39bf7f23 100644
--- a/packages/loot-core/src/server/tools/app.ts
+++ b/packages/loot-core/src/server/tools/app.ts
@@ -3,7 +3,9 @@ import { createApp } from '../app';
 import * as db from '../db';
 import { runMutator } from '../mutators';
 
-let app = createApp();
+import { ToolsHandlers } from './types/handlers';
+
+let app = createApp<ToolsHandlers>();
 
 app.method('tools/fix-split-transactions', async () => {
   // 1. Check for child transactions that have a blank payee, and set
diff --git a/packages/loot-core/src/server/tools/types/handlers.d.ts b/packages/loot-core/src/server/tools/types/handlers.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..841db08c084794cdffa97f2b4284c750bea962e4
--- /dev/null
+++ b/packages/loot-core/src/server/tools/types/handlers.d.ts
@@ -0,0 +1,7 @@
+export interface ToolsHandlers {
+  'tools/fix-split-transactions': () => Promise<{
+    numBlankPayees: number;
+    numCleared: number;
+    numDeleted: number;
+  }>;
+}
diff --git a/packages/loot-core/src/server/undo.ts b/packages/loot-core/src/server/undo.ts
index bfc67cc03b38f48831d251ac2810e4dad464eb58..f21766309bbcec8f1911bed7fd672ec48c517a0e 100644
--- a/packages/loot-core/src/server/undo.ts
+++ b/packages/loot-core/src/server/undo.ts
@@ -7,11 +7,11 @@ import { withMutatorContext, getMutatorContext } from './mutators';
 import { sendMessages } from './sync';
 
 // A marker always sits as the first entry to simplify logic
-type MarkerMessage = { type: 'marker'; meta? };
+type MarkerMessage = { type: 'marker'; meta?: unknown };
 type MessagesMessage = {
   type: 'messages';
   messages: unknown[];
-  meta?;
+  meta?: unknown;
   oldData;
   undoTag;
 };
@@ -56,7 +56,10 @@ export function clearUndo() {
   CURSOR = 0;
 }
 
-export function withUndo(func, meta?) {
+export function withUndo<T>(
+  func: () => Promise<T>,
+  meta?: unknown,
+): Promise<T> {
   let context = getMutatorContext();
   if (context.undoDisabled || context.undoListening) {
     return func();
@@ -64,7 +67,7 @@ export function withUndo(func, meta?) {
 
   MESSAGE_HISTORY = MESSAGE_HISTORY.slice(0, CURSOR + 1);
 
-  let marker = { type: 'marker' as const, meta };
+  let marker: MarkerMessage = { type: 'marker', meta };
 
   if (MESSAGE_HISTORY[MESSAGE_HISTORY.length - 1].type === 'marker') {
     MESSAGE_HISTORY[MESSAGE_HISTORY.length - 1] = marker;
@@ -79,8 +82,16 @@ export function withUndo(func, meta?) {
   );
 }
 
-export function undoable(func) {
-  return (...args) => {
+// for some reason `void` is not inferred properly without this overload
+export function undoable<Args extends unknown[]>(
+  func: (...args: Args) => Promise<void>,
+): (...args: Args) => Promise<void>;
+export function undoable<
+  Args extends unknown[],
+  Return extends Promise<unknown>,
+>(func: (...args: Args) => Return): (...args: Args) => Return;
+export function undoable(func: (...args: unknown[]) => Promise<unknown>) {
+  return (...args: unknown[]) => {
     return withUndo(() => {
       return func(...args);
     });
diff --git a/packages/loot-core/src/shared/errors.ts b/packages/loot-core/src/shared/errors.ts
index 919c21002cc83c2a281e03bf66b4bfb752cb7cbb..61b16f4f15dfd99c2ae7604a46ceb64d423d6d50 100644
--- a/packages/loot-core/src/shared/errors.ts
+++ b/packages/loot-core/src/shared/errors.ts
@@ -1,9 +1,15 @@
-export function getUploadError({ reason, meta }) {
+export function getUploadError({
+  reason,
+  meta,
+}: {
+  reason: string;
+  meta?: unknown;
+}) {
   switch (reason) {
     case 'unauthorized':
       return 'You are not logged in.';
     case 'encrypt-failure':
-      if (meta.isMissingKey) {
+      if ((meta as { isMissingKey: boolean }).isMissingKey) {
         return 'Encrypting your file failed because you are missing your encryption key. Create your key in the next step.';
       }
       return 'Encrypting the file failed. You have the correct key so this is an internal bug. To fix this, generate a new key in the next step.';
diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts
index b37b93ec56229531b4a1d2e48963fe0fa223f273..275f74965e58705c8dda2d02c1c14e8a90109b41 100644
--- a/packages/loot-core/src/shared/months.ts
+++ b/packages/loot-core/src/shared/months.ts
@@ -1,7 +1,7 @@
 import * as d from 'date-fns';
 import memoizeOne from 'memoize-one';
 
-export function _parse(value: string | Date) {
+export function _parse(value: string | number | Date) {
   if (typeof value === 'string') {
     // Dates are hard. We just want to deal with months in the format
     // 2020-01 and days in the format 2020-01-01, but life is never
@@ -66,24 +66,27 @@ export function _parse(value: string | Date) {
       return new Date(parseInt(year), 0, 1, 12);
     }
   }
+  if (typeof value === 'number') {
+    return new Date(value);
+  }
   return value;
 }
 
 export const parseDate = _parse;
 
-export function yearFromDate(date) {
+export function yearFromDate(date: string | number | Date) {
   return d.format(_parse(date), 'yyyy');
 }
 
-export function monthFromDate(date) {
+export function monthFromDate(date: string | number | Date) {
   return d.format(_parse(date), 'yyyy-MM');
 }
 
-export function dayFromDate(date) {
+export function dayFromDate(date: string | number | Date) {
   return d.format(_parse(date), 'yyyy-MM-dd');
 }
 
-export function currentMonth() {
+export function currentMonth(): string {
   if (global.IS_TESTING) {
     return global.currentMonth || '2017-01';
   } else {
@@ -99,57 +102,64 @@ export function currentDay() {
   }
 }
 
-export function nextMonth(month) {
+export function nextMonth(month: string | Date) {
   return d.format(d.addMonths(_parse(month), 1), 'yyyy-MM');
 }
 
-export function prevMonth(month) {
+export function prevMonth(month: string | Date) {
   return d.format(d.subMonths(_parse(month), 1), 'yyyy-MM');
 }
 
-export function addMonths(month, n) {
+export function addMonths(month: string | Date, n: number) {
   return d.format(d.addMonths(_parse(month), n), 'yyyy-MM');
 }
 
-export function addWeeks(date, n) {
+export function addWeeks(date: string | Date, n: number) {
   return d.format(d.addWeeks(_parse(date), n), 'yyyy-MM-dd');
 }
 
-export function differenceInCalendarMonths(month1, month2) {
+export function differenceInCalendarMonths(
+  month1: string | Date,
+  month2: string | Date,
+) {
   return d.differenceInCalendarMonths(_parse(month1), _parse(month2));
 }
 
-export function subMonths(month, n) {
+export function subMonths(month: string | Date, n: number) {
   return d.format(d.subMonths(_parse(month), n), 'yyyy-MM');
 }
 
-export function addDays(day, n) {
+export function addDays(day: string | Date, n: number) {
   return d.format(d.addDays(_parse(day), n), 'yyyy-MM-dd');
 }
 
-export function subDays(day, n) {
+export function subDays(day: string | Date, n: number) {
   return d.format(d.subDays(_parse(day), n), 'yyyy-MM-dd');
 }
 
-export function isBefore(month1, month2) {
+export function isBefore(month1: string | Date, month2: string | Date) {
   return d.isBefore(_parse(month1), _parse(month2));
 }
 
-export function isAfter(month1, month2) {
+export function isAfter(month1: string | Date, month2: string | Date) {
   return d.isAfter(_parse(month1), _parse(month2));
 }
 
 // TODO: This doesn't really fit in this module anymore, should
 // probably live elsewhere
-export function bounds(month) {
+export function bounds(month: string | Date) {
   return {
     start: parseInt(d.format(d.startOfMonth(_parse(month)), 'yyyyMMdd')),
     end: parseInt(d.format(d.endOfMonth(_parse(month)), 'yyyyMMdd')),
   };
 }
 
-export function _range(start, end, inclusive = false) {
-  const months = [];
+export function _range(
+  start: string | Date,
+  end: string | Date,
+  inclusive = false,
+): string[] {
+  const months: string[] = [];
   let month = monthFromDate(start);
   while (d.isBefore(_parse(month), _parse(end))) {
     months.push(month);
@@ -163,16 +173,20 @@ export function _range(start, end, inclusive = false) {
   return months;
 }
 
-export function range(start, end) {
+export function range(start: string, end: string) {
   return _range(start, end);
 }
 
-export function rangeInclusive(start, end) {
+export function rangeInclusive(start: string, end: string) {
   return _range(start, end, true);
 }
 
-export function _dayRange(start, end, inclusive = false) {
-  const days = [];
+export function _dayRange(
+  start: string,
+  end: string | Date,
+  inclusive = false,
+): string[] {
+  const days: string[] = [];
   let day = start;
   while (d.isBefore(_parse(day), _parse(end))) {
     days.push(day);
@@ -186,48 +200,48 @@ export function _dayRange(start, end, inclusive = false) {
   return days;
 }
 
-export function dayRange(start, end) {
+export function dayRange(start: string, end: string) {
   return _dayRange(start, end);
 }
 
-export function dayRangeInclusive(start, end) {
+export function dayRangeInclusive(start: string, end: string) {
   return _dayRange(start, end, true);
 }
 
-export function getMonthIndex(month) {
+export function getMonthIndex(month: string) {
   return parseInt(month.slice(5, 7)) - 1;
 }
 
-export function getYear(month) {
+export function getYear(month: string) {
   return month.slice(0, 4);
 }
 
-export function getMonth(day) {
+export function getMonth(day: string) {
   return day.slice(0, 7);
 }
 
-export function getYearStart(month) {
+export function getYearStart(month: string) {
   return getYear(month) + '-01';
 }
 
-export function getYearEnd(month) {
+export function getYearEnd(month: string) {
   return getYear(month) + '-12';
 }
 
-export function sheetForMonth(month) {
+export function sheetForMonth(month: string) {
   return 'budget' + month.replace('-', '');
 }
 
-export function nameForMonth(month) {
+export function nameForMonth(month: string | Date) {
   // eslint-disable-next-line rulesdir/typography
   return d.format(_parse(month), "MMMM 'yy");
 }
 
-export function format(month, str) {
+export function format(month: string | Date, str: string) {
   return d.format(_parse(month), str);
 }
 
-export const getDateFormatRegex = memoizeOne(format => {
+export const getDateFormatRegex = memoizeOne((format: string) => {
   return new RegExp(
     format
       .replace(/d+/g, '\\d{1,2}')
@@ -236,14 +250,14 @@ export const getDateFormatRegex = memoizeOne(format => {
   );
 });
 
-export const getDayMonthFormat = memoizeOne(format => {
+export const getDayMonthFormat = memoizeOne((format: string) => {
   return format
     .replace(/y+/g, '')
     .replace(/[^\w]$/, '')
     .replace(/^[^\w]/, '');
 });
 
-export const getDayMonthRegex = memoizeOne(format => {
+export const getDayMonthRegex = memoizeOne((format: string) => {
   let regex = format
     .replace(/y+/g, '')
     .replace(/[^\w]$/, '')
@@ -253,7 +267,7 @@ export const getDayMonthRegex = memoizeOne(format => {
   return new RegExp('^' + regex + '$');
 });
 
-export const getMonthYearFormat = memoizeOne(format => {
+export const getMonthYearFormat = memoizeOne((format: string) => {
   return format
     .replace(/d+/g, '')
     .replace(/[^\w]$/, '')
@@ -263,7 +277,7 @@ export const getMonthYearFormat = memoizeOne(format => {
     .replace(/--/, '-');
 });
 
-export const getMonthYearRegex = memoizeOne(format => {
+export const getMonthYearRegex = memoizeOne((format: string) => {
   let regex = format
     .replace(/d+/g, '')
     .replace(/[^\w]$/, '')
@@ -274,11 +288,11 @@ export const getMonthYearRegex = memoizeOne(format => {
   return new RegExp('^' + regex + '$');
 });
 
-export const getShortYearFormat = memoizeOne(format => {
+export const getShortYearFormat = memoizeOne((format: string) => {
   return format.replace(/y+/g, 'yy');
 });
 
-export const getShortYearRegex = memoizeOne(format => {
+export const getShortYearRegex = memoizeOne((format: string) => {
   let regex = format
     .replace(/[^\w]$/, '')
     .replace(/^[^\w]/, '')
diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts
index d77ae3613df62d456333f1f6270bd4da50bb633c..81dd0f2c118379dbf529d03f8803738c59bad5dc 100644
--- a/packages/loot-core/src/types/handlers.d.ts
+++ b/packages/loot-core/src/types/handlers.d.ts
@@ -1,13 +1,15 @@
 import type { BudgetHandlers } from '../server/budget/types/handlers';
 import type { NotesHandlers } from '../server/notes/types/handlers';
 import type { SchedulesHandlers } from '../server/schedules/types/handlers';
+import type { ToolsHandlers } from '../server/tools/types/handlers';
 
 import type { ApiHandlers } from './api-handlers';
-import type { MainHandlers } from './main-handlers';
+import type { ServerHandlers } from './server-handlers';
 
 export interface Handlers
-  extends MainHandlers,
+  extends ServerHandlers,
     ApiHandlers,
     BudgetHandlers,
     NotesHandlers,
-    SchedulesHandlers {}
+    SchedulesHandlers,
+    ToolsHandlers {}
diff --git a/packages/loot-core/src/types/main-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts
similarity index 66%
rename from packages/loot-core/src/types/main-handlers.d.ts
rename to packages/loot-core/src/types/server-handlers.d.ts
index 3925c8d8d61a9315b3786a2690cd57839a8bfba6..061e40353dc4a1dd7c64b7a52be2b90c371dc955 100644
--- a/packages/loot-core/src/types/main-handlers.d.ts
+++ b/packages/loot-core/src/types/server-handlers.d.ts
@@ -1,30 +1,39 @@
-export interface MainHandlers {
-  'transaction-update': (
-    transaction: unknown,
-  ) => Promise<Record<string, never>>;
+import { ParseFileResult } from '../server/accounts/parse-file';
+import { batchUpdateTransactions } from '../server/accounts/transactions';
+import { Backup } from '../server/backups';
+import { RemoteFile } from '../server/cloud-storage';
+import { Message } from '../server/sync';
 
-  undo: () => Promise<unknown>;
+import { EmptyObject } from './util';
 
-  redo: () => Promise<unknown>;
+export interface ServerHandlers {
+  'transaction-update': (transaction: { id: string }) => Promise<EmptyObject>;
 
-  'transactions-batch-update': (arg: {
-    added;
-    deleted;
-    updated;
-    learnCategories;
-  }) => Promise<unknown>;
+  undo: () => Promise<void>;
+
+  redo: () => Promise<void>;
+
+  'transactions-batch-update': (
+    arg: Omit<
+      Parameters<typeof batchUpdateTransactions>[0],
+      'detectOrphanPayees'
+    >,
+  ) => Promise<Awaited<ReturnType<typeof batchUpdateTransactions>>['updated']>;
 
-  'transaction-add': (transaction) => Promise<Record<string, never>>;
+  'transaction-add': (transaction) => Promise<EmptyObject>;
 
-  'transaction-aupdatedd': (transaction) => Promise<Record<string, never>>;
+  'transaction-add': (transaction) => Promise<EmptyObject>;
 
-  'transaction-delete': (transaction) => Promise<Record<string, never>>;
+  'transaction-delete': (transaction) => Promise<EmptyObject>;
 
-  'transactions-parse-file': (arg: { filepath; options }) => Promise<unknown>;
+  'transactions-parse-file': (arg: {
+    filepath: string;
+    options;
+  }) => Promise<ParseFileResult>;
 
   'transactions-export': (arg: {
     transactions;
-    accounts;
+    accounts?;
     categoryGroups;
     payees;
   }) => Promise<unknown>;
@@ -32,13 +41,13 @@ export interface MainHandlers {
   'transactions-export-query': (arg: { query: queryState }) => Promise<unknown>;
 
   'get-categories': () => Promise<{
-    grouped: unknown;
-    list: unknown;
+    grouped: unknown[];
+    list: unknown[];
   }>;
 
   'get-earliest-transaction': () => Promise<unknown>;
 
-  'get-budget-bounds': () => Promise<unknown>;
+  'get-budget-bounds': () => Promise<{ start: string; end: string }>;
 
   'rollover-budget-month': (arg: { month }) => Promise<unknown>;
 
@@ -52,7 +61,7 @@ export interface MainHandlers {
 
   'category-move': (arg: { id; groupId; targetId }) => Promise<unknown>;
 
-  'category-delete': (arg: { id; transferId }) => Promise<{ error }>;
+  'category-delete': (arg: { id; transferId }) => Promise<{ error?: string }>;
 
   'category-group-create': (arg: {
     name;
@@ -69,13 +78,17 @@ export interface MainHandlers {
 
   'payee-create': (arg: { name }) => Promise<unknown>;
 
-  'payees-get': () => Promise<unknown>;
+  'payees-get': () => Promise<unknown[]>;
 
   'payees-get-rule-counts': () => Promise<unknown>;
 
   'payees-merge': (arg: { targetId; mergeIds }) => Promise<unknown>;
 
-  'payees-batch-change': (arg: { added; deleted; updated }) => Promise<unknown>;
+  'payees-batch-change': (arg: {
+    added?;
+    deleted?;
+    updated?;
+  }) => Promise<unknown>;
 
   'payees-check-orphaned': (arg: { ids }) => Promise<unknown>;
 
@@ -163,9 +176,9 @@ export interface MainHandlers {
 
   'account-close': (arg: {
     id;
-    transferAccountId;
-    categoryId;
-    forced;
+    transferAccountId?;
+    categoryId?;
+    forced?;
   }) => Promise<unknown>;
 
   'account-reopen': (arg: { id }) => Promise<unknown>;
@@ -184,12 +197,12 @@ export interface MainHandlers {
   }>;
 
   'secret-set': (arg: { name: string; value: string }) => Promise<null>;
-  'secret-check': (arg: string) => Promise<null>;
+  'secret-check': (arg: string) => Promise<string | { error?: string }>;
 
   'nordigen-poll-web-token': (arg: {
     upgradingAccountId;
     requisitionId;
-  }) => Promise<null>;
+  }) => Promise<{ error } | { data }>;
 
   'nordigen-status': () => Promise<{ configured: boolean }>;
 
@@ -211,7 +224,7 @@ export interface MainHandlers {
   }>;
 
   'transactions-import': (arg: { accountId; transactions }) => Promise<{
-    errors;
+    errors?: { message: string }[];
     added;
     updated;
   }>;
@@ -239,13 +252,16 @@ export interface MainHandlers {
 
   'load-prefs': () => Promise<Record<string, unknown> | null>;
 
-  'sync-reset': () => Promise<{ error }>;
+  'sync-reset': () => Promise<{ error?: { reason: string; meta?: unknown } }>;
 
   'sync-repair': () => Promise<unknown>;
 
   'key-make': (arg: { password }) => Promise<unknown>;
 
-  'key-test': (arg: { fileId; password }) => Promise<unknown>;
+  'key-test': (arg: {
+    fileId;
+    password;
+  }) => Promise<{ error?: { reason: string } }>;
 
   'get-did-bootstrap': () => Promise<boolean>;
 
@@ -255,37 +271,47 @@ export interface MainHandlers {
     { error: string } | { bootstrapped: unknown; hasServer: boolean }
   >;
 
-  'subscribe-bootstrap': (arg: { password }) => Promise<{ error: string }>;
+  'subscribe-bootstrap': (arg: { password }) => Promise<{ error?: string }>;
 
   'subscribe-get-user': () => Promise<{ offline: boolean } | null>;
 
   'subscribe-change-password': (arg: {
     password;
-  }) => Promise<{ error: string }>;
+  }) => Promise<{ error?: string }>;
 
-  'subscribe-sign-in': (arg: { password }) => Promise<{ error: string }>;
+  'subscribe-sign-in': (arg: { password }) => Promise<{ error?: string }>;
 
   'subscribe-sign-out': () => Promise<'ok'>;
 
-  'get-server-version': () => Promise<{ error: string } | { version: string }>;
+  'get-server-version': () => Promise<{ error?: string } | { version: string }>;
 
   'get-server-url': () => Promise<unknown>;
 
   'set-server-url': (arg: { url; validate }) => Promise<unknown>;
 
-  sync: () => Promise<{ error: string }>;
+  sync: () => Promise<
+    | { error: { message: string; reason: string; meta: unknown } }
+    | { messages: Message[] }
+  >;
 
-  'get-budgets': () => Promise<unknown>;
+  'get-budgets': () => Promise<
+    {
+      id: string;
+      cloudFileId: string;
+      groupId: string;
+      name: string;
+    }[]
+  >;
 
-  'get-remote-files': () => Promise<unknown>;
+  'get-remote-files': () => Promise<RemoteFile[]>;
 
   'reset-budget-cache': () => Promise<unknown>;
 
-  'upload-budget': (arg: { id } = {}) => Promise<{ error }>;
+  'upload-budget': (arg: { id } = {}) => Promise<{ error?: string }>;
 
-  'download-budget': (arg: { fileId; replace }) => Promise<{ error; id }>;
+  'download-budget': (arg: { fileId; replace? }) => Promise<{ error; id }>;
 
-  'sync-budget': () => Promise<Record<string, never>>;
+  'sync-budget': () => Promise<EmptyObject>;
 
   'load-budget': (arg: { id }) => Promise<{ error }>;
 
@@ -293,28 +319,34 @@ export interface MainHandlers {
 
   'close-budget': () => Promise<'ok'>;
 
-  'delete-budget': (arg: { id; cloudFileId }) => Promise<'ok'>;
+  'delete-budget': (arg: { id; cloudFileId? }) => Promise<'ok'>;
 
   'create-budget': (arg: {
     budgetName?;
     avoidUpload?;
-    testMode: boolean;
+    testMode?: boolean;
     testBudgetId?;
   }) => Promise<unknown>;
 
-  'import-budget': (arg: { filepath; type }) => Promise<{ error }>;
+  'import-budget': (arg: {
+    filepath: string;
+    type: 'ynab4' | 'ynab5' | 'actual';
+  }) => Promise<{ error?: string }>;
 
-  'export-budget': () => Promise<unknown>;
+  'export-budget': () => Promise<Buffer | null>;
 
-  'upload-file-web': (arg: { filename; contents }) => Promise<'ok'>;
+  'upload-file-web': (arg: {
+    filename: string;
+    contents: ArrayBuffer;
+  }) => Promise<EmptyObject | null>;
 
-  'backups-get': (arg: { id }) => Promise<unknown>;
+  'backups-get': (arg: { id: string }) => Promise<Backup[]>;
 
-  'backup-load': (arg: { id; backupId }) => Promise<unknown>;
+  'backup-load': (arg: { id: string; backupId: string }) => Promise<void>;
 
-  'backup-make': (arg: { id }) => Promise<unknown>;
+  'backup-make': (arg: { id: string }) => Promise<void>;
 
-  'get-last-opened-backup': () => Promise<unknown>;
+  'get-last-opened-backup': () => Promise<string | null>;
 
-  'app-focused': () => Promise<unknown>;
+  'app-focused': () => Promise<void>;
 }
diff --git a/packages/loot-core/src/types/util.d.ts b/packages/loot-core/src/types/util.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..253e96456b8eafdbb7e9e15e51b35409cad47b68
--- /dev/null
+++ b/packages/loot-core/src/types/util.d.ts
@@ -0,0 +1 @@
+export type EmptyObject = Record<string, never>;
diff --git a/upcoming-release-notes/1180.md b/upcoming-release-notes/1180.md
new file mode 100644
index 0000000000000000000000000000000000000000..f7c95d9765cf563b7320963ddfe33e4bf2ccf3b0
--- /dev/null
+++ b/upcoming-release-notes/1180.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Improve TypeScript types in `loot-core`
diff --git a/yarn.lock b/yarn.lock
index 337413a52dfcfc2f3c7a7bdc1e5ca6c9568673f3..e846cfcd263f28a55ffd2318e4204a4f2361276c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -97,7 +97,7 @@ __metadata:
     inter-ui: ^3.19.3
     jest: ^27.0.0
     jest-watch-typeahead: ^2.2.2
-    memoize-one: ^4.0.0
+    memoize-one: ^6.0.0
     pikaday: 1.8.0
     react: 18.2.0
     react-app-rewired: ^2.2.1
@@ -12357,7 +12357,7 @@ __metadata:
     lru-cache: ^5.1.1
     md5: ^2.3.0
     memfs: 3.1.1
-    memoize-one: ^4.0.0
+    memoize-one: ^6.0.0
     mitt: ^3.0.0
     mockdate: ^3.0.5
     node-fetch: ^2.6.9
@@ -12579,10 +12579,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"memoize-one@npm:^4.0.0":
-  version: 4.0.3
-  resolution: "memoize-one@npm:4.0.3"
-  checksum: addd18c046542f57440ba70bf8ebd48663d17626cade681f777522ef70900a87ec72c5041bed8ece4f6d40a2cb58803bae388b50a4b740d64f36bcda20c147b7
+"memoize-one@npm:^6.0.0":
+  version: 6.0.0
+  resolution: "memoize-one@npm:6.0.0"
+  checksum: f185ea69f7cceae5d1cb596266dcffccf545e8e7b4106ec6aa93b71ab9d16460dd118ac8b12982c55f6d6322fcc1485de139df07eacffaae94888b9b3ad7675f
   languageName: node
   linkType: hard