From 2d7e0c3f7a4bebaa639dd0b55259c075f29041c8 Mon Sep 17 00:00:00 2001
From: Alberto Gasparin <albertogasparin@gmail.com>
Date: Sun, 30 Apr 2023 09:25:45 +1000
Subject: [PATCH] Migrate core to TS p4 (#957)

This is the last PR with lots of renaming for `loot-core`!
---
 packages/loot-core/jest.config.js             |  2 +-
 ...eetProvider.js => SpreadsheetProvider.tsx} |  2 +-
 .../loot-core/src/client/actions/budgets.ts   |  1 -
 .../src/client/{constants.js => constants.ts} |  0
 .../src/client/{platform.js => platform.ts}   |  2 +-
 .../src/client/{queries.js => queries.ts}     |  0
 ...-helpers.test.js => query-helpers.test.ts} |  7 ++-
 .../{query-helpers.js => query-helpers.ts}    | 33 ++++++++++--
 .../{query-hooks.js => query-hooks.tsx}       | 30 +----------
 ...hared-listeners.js => shared-listeners.ts} |  0
 .../src/client/{tutorial.js => tutorial.ts}   |  0
 ...notification.js => update-notification.ts} |  0
 ...ifications.js => upgrade-notifications.ts} |  0
 ...rbitrary-schema.js => arbitrary-schema.ts} | 26 +++++++---
 .../src/mocks/{budget.js => budget.ts}        | 19 ++++---
 .../src/mocks/{index.js => index.ts}          |  5 +-
 .../src/mocks/{plaid.js => plaid.ts}          |  0
 .../src/mocks/{redux.js => redux.tsx}         |  0
 .../src/mocks/{setup.js => setup.ts}          |  8 +--
 .../mocks/{spreadsheet.js => spreadsheet.ts}  |  0
 .../loot-core/src/mocks/{util.js => util.ts}  |  5 +-
 .../client/fetch/__mocks__/index.web.ts       |  8 +--
 .../src/platform/client/fetch/index.d.ts      | 16 +++++-
 .../platform/server/asyncStorage/index.d.ts   |  4 +-
 .../platform/server/asyncStorage/index.web.ts |  2 +-
 .../src/platform/server/connection/index.d.ts |  9 +++-
 .../src/platform/server/fs/index.d.ts         |  5 +-
 .../src/platform/server/fs/index.electron.ts  |  9 +++-
 ...kups.test.js.snap => backups.test.ts.snap} |  0
 .../{main.test.js.snap => main.test.ts.snap}  |  0
 ...{sheet.test.js.snap => sheet.test.ts.snap} |  0
 .../server/{api-models.js => api-models.ts}   |  2 +-
 .../loot-core/src/server/{api.js => api.ts}   |  8 +--
 .../loot-core/src/server/{app.js => app.ts}   |  5 ++
 .../{backups.test.js => backups.test.ts}      |  0
 .../src/server/{backups.js => backups.ts}     |  4 +-
 .../src/server/{bench.js => bench.ts}         |  4 +-
 .../{cloud-storage.js => cloud-storage.ts}    |  8 +--
 packages/loot-core/src/server/db/index.ts     |  6 +--
 ...n-internals.js => encryption-internals.ts} | 15 ++++--
 ...als.web.js => encryption-internals.web.ts} |  2 +
 ...{encryption.test.js => encryption.test.ts} |  0
 .../server/{encryption.js => encryption.ts}   |  3 ++
 .../src/server/{errors.js => errors.ts}       | 33 +++++++-----
 .../src/server/{main-app.js => main-app.ts}   |  0
 .../src/server/{main.test.js => main.test.ts} |  5 +-
 .../loot-core/src/server/{main.js => main.ts} | 52 +++++++------------
 .../src/server/{models.js => models.ts}       | 12 ++---
 .../src/server/{mutators.js => mutators.ts}   | 17 ++++--
 .../loot-core/src/server/{perf.js => perf.ts} |  0
 .../src/server/{platform.js => platform.ts}   |  0
 .../{platform.web.js => platform.web.ts}      |  0
 .../src/server/{polyfills.js => polyfills.ts} |  0
 packages/loot-core/src/server/post.ts         |  2 +-
 .../src/server/{prefs.js => prefs.ts}         |  2 +-
 .../{server-config.js => server-config.ts}    |  2 +-
 .../server/{sheet.test.js => sheet.test.ts}   |  8 +--
 .../src/server/{sheet.js => sheet.ts}         | 10 ++--
 packages/loot-core/src/server/sync/index.ts   |  5 +-
 packages/loot-core/src/server/sync/reset.ts   |  2 +-
 .../loot-core/src/server/{undo.js => undo.ts} | 22 +++++---
 .../src/server/{update.js => update.ts}       |  0
 packages/loot-core/src/shared/months.ts       |  2 +-
 packages/loot-core/src/shared/test-helpers.ts |  2 +-
 ...nsactions.test.js => transactions.test.ts} |  0
 packages/loot-core/src/types/handlers.d.ts    | 11 ++--
 .../loot-core/src/types/main.handlers.d.ts    | 11 +---
 .../loot-core/src/types/models/account.d.ts   | 11 ++++
 .../src/types/models/category-group.d.ts      |  9 ++++
 .../loot-core/src/types/models/category.d.ts  | 10 ++++
 .../loot-core/src/types/models/index.d.ts     |  6 +++
 .../loot-core/src/types/models/payee.d.ts     | 10 ++++
 packages/loot-core/src/types/models/rule.d.ts |  8 +++
 .../loot-core/src/types/models/schedule.d.ts  | 23 ++++++++
 .../src/types/models/transaction.d.ts         | 29 +++++++++++
 .../loot-core/src/types/server-events.d.ts    | 16 ++++++
 .../{typings.d.ts => typings/pegjs.d.ts}      |  0
 packages/loot-core/typings/window.d.ts        | 16 ++++++
 .../webpack/webpack.browser.config.js         |  2 +-
 upcoming-release-notes/957.md                 |  6 +++
 80 files changed, 405 insertions(+), 189 deletions(-)
 rename packages/loot-core/src/client/{SpreadsheetProvider.js => SpreadsheetProvider.tsx} (98%)
 rename packages/loot-core/src/client/{constants.js => constants.ts} (100%)
 rename packages/loot-core/src/client/{platform.js => platform.ts} (90%)
 rename packages/loot-core/src/client/{queries.js => queries.ts} (100%)
 rename packages/loot-core/src/client/{query-helpers.test.js => query-helpers.test.ts} (98%)
 rename packages/loot-core/src/client/{query-helpers.js => query-helpers.ts} (93%)
 rename packages/loot-core/src/client/{query-hooks.js => query-hooks.tsx} (76%)
 rename packages/loot-core/src/client/{shared-listeners.js => shared-listeners.ts} (100%)
 rename packages/loot-core/src/client/{tutorial.js => tutorial.ts} (100%)
 rename packages/loot-core/src/client/{update-notification.js => update-notification.ts} (100%)
 rename packages/loot-core/src/client/{upgrade-notifications.js => upgrade-notifications.ts} (100%)
 rename packages/loot-core/src/mocks/{arbitrary-schema.js => arbitrary-schema.ts} (83%)
 rename packages/loot-core/src/mocks/{budget.js => budget.ts} (98%)
 rename packages/loot-core/src/mocks/{index.js => index.ts} (94%)
 rename packages/loot-core/src/mocks/{plaid.js => plaid.ts} (100%)
 rename packages/loot-core/src/mocks/{redux.js => redux.tsx} (100%)
 rename packages/loot-core/src/mocks/{setup.js => setup.ts} (95%)
 rename packages/loot-core/src/mocks/{spreadsheet.js => spreadsheet.ts} (100%)
 rename packages/loot-core/src/mocks/{util.js => util.ts} (91%)
 rename packages/loot-core/src/server/__snapshots__/{backups.test.js.snap => backups.test.ts.snap} (100%)
 rename packages/loot-core/src/server/__snapshots__/{main.test.js.snap => main.test.ts.snap} (100%)
 rename packages/loot-core/src/server/__snapshots__/{sheet.test.js.snap => sheet.test.ts.snap} (100%)
 rename packages/loot-core/src/server/{api-models.js => api-models.ts} (98%)
 rename packages/loot-core/src/server/{api.js => api.ts} (98%)
 rename packages/loot-core/src/server/{app.js => app.ts} (96%)
 rename packages/loot-core/src/server/{backups.test.js => backups.test.ts} (100%)
 rename packages/loot-core/src/server/{backups.js => backups.ts} (99%)
 rename packages/loot-core/src/server/{bench.js => bench.ts} (86%)
 rename packages/loot-core/src/server/{cloud-storage.js => cloud-storage.ts} (98%)
 rename packages/loot-core/src/server/{encryption-internals.js => encryption-internals.ts} (85%)
 rename packages/loot-core/src/server/{encryption-internals.web.js => encryption-internals.web.ts} (95%)
 rename packages/loot-core/src/server/{encryption.test.js => encryption.test.ts} (100%)
 rename packages/loot-core/src/server/{encryption.js => encryption.ts} (99%)
 rename packages/loot-core/src/server/{errors.js => errors.ts} (68%)
 rename packages/loot-core/src/server/{main-app.js => main-app.ts} (100%)
 rename packages/loot-core/src/server/{main.test.js => main.test.ts} (99%)
 rename packages/loot-core/src/server/{main.js => main.ts} (98%)
 rename packages/loot-core/src/server/{models.js => models.ts} (92%)
 rename packages/loot-core/src/server/{mutators.js => mutators.ts} (90%)
 rename packages/loot-core/src/server/{perf.js => perf.ts} (100%)
 rename packages/loot-core/src/server/{platform.js => platform.ts} (100%)
 rename packages/loot-core/src/server/{platform.web.js => platform.web.ts} (100%)
 rename packages/loot-core/src/server/{polyfills.js => polyfills.ts} (100%)
 rename packages/loot-core/src/server/{prefs.js => prefs.ts} (98%)
 rename packages/loot-core/src/server/{server-config.js => server-config.ts} (94%)
 rename packages/loot-core/src/server/{sheet.test.js => sheet.test.ts} (95%)
 rename packages/loot-core/src/server/{sheet.js => sheet.ts} (95%)
 rename packages/loot-core/src/server/{undo.js => undo.ts} (92%)
 rename packages/loot-core/src/server/{update.js => update.ts} (100%)
 rename packages/loot-core/src/shared/{transactions.test.js => transactions.test.ts} (100%)
 create mode 100644 packages/loot-core/src/types/models/account.d.ts
 create mode 100644 packages/loot-core/src/types/models/category-group.d.ts
 create mode 100644 packages/loot-core/src/types/models/category.d.ts
 create mode 100644 packages/loot-core/src/types/models/index.d.ts
 create mode 100644 packages/loot-core/src/types/models/payee.d.ts
 create mode 100644 packages/loot-core/src/types/models/rule.d.ts
 create mode 100644 packages/loot-core/src/types/models/schedule.d.ts
 create mode 100644 packages/loot-core/src/types/models/transaction.d.ts
 create mode 100644 packages/loot-core/src/types/server-events.d.ts
 rename packages/loot-core/{typings.d.ts => typings/pegjs.d.ts} (100%)
 create mode 100644 packages/loot-core/typings/window.d.ts
 create mode 100644 upcoming-release-notes/957.md

diff --git a/packages/loot-core/jest.config.js b/packages/loot-core/jest.config.js
index 1a8248b70..f62c2c4dc 100644
--- a/packages/loot-core/jest.config.js
+++ b/packages/loot-core/jest.config.js
@@ -11,7 +11,7 @@ module.exports = {
     'tsx',
     'json',
   ],
-  setupFilesAfterEnv: ['<rootDir>/src/mocks/setup.js'],
+  setupFilesAfterEnv: ['<rootDir>/src/mocks/setup.ts'],
   testEnvironment: 'node',
   testPathIgnorePatterns: [
     '/node_modules/',
diff --git a/packages/loot-core/src/client/SpreadsheetProvider.js b/packages/loot-core/src/client/SpreadsheetProvider.tsx
similarity index 98%
rename from packages/loot-core/src/client/SpreadsheetProvider.js
rename to packages/loot-core/src/client/SpreadsheetProvider.tsx
index 36d3cb9a2..1494c663d 100644
--- a/packages/loot-core/src/client/SpreadsheetProvider.js
+++ b/packages/loot-core/src/client/SpreadsheetProvider.tsx
@@ -77,7 +77,7 @@ function makeSpreadsheet() {
       if (cellCache[resolvedName] != null) {
         cellCache[resolvedName].then(cb);
       } else {
-        const req = this.get(sheetName, binding.name, fields);
+        const req = this.get(sheetName, binding.name);
         cellCache[resolvedName] = req;
 
         req.then(result => {
diff --git a/packages/loot-core/src/client/actions/budgets.ts b/packages/loot-core/src/client/actions/budgets.ts
index aea8292df..5eff493d1 100644
--- a/packages/loot-core/src/client/actions/budgets.ts
+++ b/packages/loot-core/src/client/actions/budgets.ts
@@ -165,7 +165,6 @@ export function importBudget(filepath, type) {
     dispatch(closeModal());
 
     await dispatch(loadPrefs());
-    // @ts-expect-error __history needs refinement
     window.__history.push('/budget');
   };
 }
diff --git a/packages/loot-core/src/client/constants.js b/packages/loot-core/src/client/constants.ts
similarity index 100%
rename from packages/loot-core/src/client/constants.js
rename to packages/loot-core/src/client/constants.ts
diff --git a/packages/loot-core/src/client/platform.js b/packages/loot-core/src/client/platform.ts
similarity index 90%
rename from packages/loot-core/src/client/platform.js
rename to packages/loot-core/src/client/platform.ts
index 20e2fd00b..ba0a61a7c 100644
--- a/packages/loot-core/src/client/platform.js
+++ b/packages/loot-core/src/client/platform.ts
@@ -9,5 +9,5 @@ export const isProbablySafari = /^((?!chrome|android).)*safari/i.test(
 );
 
 export const OS = isWindows ? 'windows' : isMac ? 'mac' : 'linux';
-export const env = 'web';
+export const env: 'web' | 'mobile' = 'web';
 export const isBrowser = !!window.Actual?.IS_FAKE_WEB;
diff --git a/packages/loot-core/src/client/queries.js b/packages/loot-core/src/client/queries.ts
similarity index 100%
rename from packages/loot-core/src/client/queries.js
rename to packages/loot-core/src/client/queries.ts
diff --git a/packages/loot-core/src/client/query-helpers.test.js b/packages/loot-core/src/client/query-helpers.test.ts
similarity index 98%
rename from packages/loot-core/src/client/query-helpers.test.js
rename to packages/loot-core/src/client/query-helpers.test.ts
index 9b72a3fb0..13b42a06f 100644
--- a/packages/loot-core/src/client/query-helpers.test.js
+++ b/packages/loot-core/src/client/query-helpers.test.ts
@@ -77,7 +77,7 @@ function runPagedQuery(query, data) {
   throw new Error('Unable to execute query: ' + JSON.stringify(query, null, 2));
 }
 
-function initBasicServer(delay) {
+function initBasicServer(delay?) {
   initServer({
     query: async query => {
       if (!isCountQuery(query)) {
@@ -91,7 +91,10 @@ function initBasicServer(delay) {
   });
 }
 
-function initPagingServer(dataLength, { delay, eventType = 'select' } = {}) {
+function initPagingServer(
+  dataLength,
+  { delay, eventType = 'select' }: { delay?: number; eventType?: string } = {},
+) {
   let data = [];
   for (let i = 0; i < dataLength; i++) {
     data.push({ id: i, date: subDays('2020-05-01', Math.floor(i / 5)) });
diff --git a/packages/loot-core/src/client/query-helpers.js b/packages/loot-core/src/client/query-helpers.ts
similarity index 93%
rename from packages/loot-core/src/client/query-helpers.js
rename to packages/loot-core/src/client/query-helpers.ts
index 635d9c75e..4416d3851 100644
--- a/packages/loot-core/src/client/query-helpers.js
+++ b/packages/loot-core/src/client/query-helpers.ts
@@ -7,13 +7,13 @@ export async function runQuery(query) {
   return send('query', query.serialize());
 }
 
-export function liveQuery(query, onData, opts) {
+export function liveQuery(query, onData?, opts?) {
   let q = new LiveQuery(query, onData, opts);
   q.run();
   return q;
 }
 
-export function pagedQuery(query, onData, opts) {
+export function pagedQuery(query, onData?, opts?) {
   let q = new PagedQuery(query, onData, opts);
   q.run();
   return q;
@@ -21,7 +21,22 @@ export function pagedQuery(query, onData, opts) {
 
 // Subscribe and refetch
 export class LiveQuery {
-  constructor(query, onData, opts = {}) {
+  _unsubscribe;
+  data;
+  dependencies;
+  error;
+  listeners;
+  mappedData;
+  mapper;
+  onlySync;
+  query;
+
+  // Async coordination
+  inflight;
+  inflightRequestId;
+  restart;
+
+  constructor(query, onData?, opts: { mapper?; onlySync?: boolean } = {}) {
     this.error = new Error();
     this.query = query;
     this.data = null;
@@ -162,7 +177,17 @@ export class LiveQuery {
 
 // Paging
 export class PagedQuery extends LiveQuery {
-  constructor(query, onData, opts = {}) {
+  done;
+  onPageData;
+  pageCount;
+  runPromise;
+  totalCount;
+
+  constructor(
+    query,
+    onData,
+    opts: { pageCount?: number; onPageData?; mapper?; onlySync? } = {},
+  ) {
     super(query, onData, opts);
     this.totalCount = null;
     this.pageCount = opts.pageCount || 500;
diff --git a/packages/loot-core/src/client/query-hooks.js b/packages/loot-core/src/client/query-hooks.tsx
similarity index 76%
rename from packages/loot-core/src/client/query-hooks.js
rename to packages/loot-core/src/client/query-hooks.tsx
index c141b2abf..f5f3e72b0 100644
--- a/packages/loot-core/src/client/query-hooks.js
+++ b/packages/loot-core/src/client/query-hooks.tsx
@@ -1,6 +1,6 @@
 import React, { useState, useContext, useMemo, useEffect } from 'react';
 
-import { runQuery, liveQuery, LiveQuery, PagedQuery } from './query-helpers';
+import { liveQuery, LiveQuery, PagedQuery } from './query-helpers';
 
 function makeContext(queryState, opts, QueryClass) {
   let query = new QueryClass(queryState, null, opts);
@@ -56,34 +56,6 @@ function makeContext(queryState, opts, QueryClass) {
   };
 }
 
-export function queryContext(queryState, opts) {
-  let Context = React.createContext(null);
-
-  function Provider({ children }) {
-    let [data, setData] = useState(null);
-    let value = useMemo(() => ({ data }), [data]);
-
-    useEffect(() => {
-      async function run() {
-        let { data } = await runQuery(queryState, opts);
-        setData(data);
-      }
-      run();
-    }, []);
-
-    return <Context.Provider value={value} children={children} />;
-  }
-
-  function useQuery() {
-    return useContext(Context);
-  }
-
-  return {
-    Provider,
-    useQuery,
-  };
-}
-
 export function liveQueryContext(query, opts) {
   return makeContext(query, opts, LiveQuery);
 }
diff --git a/packages/loot-core/src/client/shared-listeners.js b/packages/loot-core/src/client/shared-listeners.ts
similarity index 100%
rename from packages/loot-core/src/client/shared-listeners.js
rename to packages/loot-core/src/client/shared-listeners.ts
diff --git a/packages/loot-core/src/client/tutorial.js b/packages/loot-core/src/client/tutorial.ts
similarity index 100%
rename from packages/loot-core/src/client/tutorial.js
rename to packages/loot-core/src/client/tutorial.ts
diff --git a/packages/loot-core/src/client/update-notification.js b/packages/loot-core/src/client/update-notification.ts
similarity index 100%
rename from packages/loot-core/src/client/update-notification.js
rename to packages/loot-core/src/client/update-notification.ts
diff --git a/packages/loot-core/src/client/upgrade-notifications.js b/packages/loot-core/src/client/upgrade-notifications.ts
similarity index 100%
rename from packages/loot-core/src/client/upgrade-notifications.js
rename to packages/loot-core/src/client/upgrade-notifications.ts
diff --git a/packages/loot-core/src/mocks/arbitrary-schema.js b/packages/loot-core/src/mocks/arbitrary-schema.ts
similarity index 83%
rename from packages/loot-core/src/mocks/arbitrary-schema.js
rename to packages/loot-core/src/mocks/arbitrary-schema.ts
index 282f65384..97ec3113d 100644
--- a/packages/loot-core/src/mocks/arbitrary-schema.js
+++ b/packages/loot-core/src/mocks/arbitrary-schema.ts
@@ -1,9 +1,9 @@
-import fc from 'fast-check';
+import fc, { type Arbitrary } from 'fast-check';
 
 import { schema } from '../server/aql';
 import { addDays } from '../shared/months';
 
-export function typeArbitrary(typeDesc, name) {
+export function typeArbitrary(typeDesc, name?) {
   let arb;
   switch (typeDesc.type) {
     case 'id':
@@ -93,12 +93,19 @@ export function flattenSortTransactions(arr) {
   });
 }
 
-function tableArbitrary(tableSchema, extraArbs, requiredKeys = []) {
+function tableArbitrary<
+  T extends Record<string, { type: string; required?: boolean }>,
+  E extends Record<string, Arbitrary<unknown>>,
+>(
+  tableSchema: T,
+  extraArbs?: E,
+  requiredKeys: Array<Extract<keyof T | keyof E, string>> = [],
+) {
   let arb = fc.record(
     {
-      ...Object.fromEntries(
+      ...Object.fromEntries<T>(
         Object.entries(tableSchema).map(([name, field]) => {
-          return [name, typeArbitrary(field, name)];
+          return [name, typeArbitrary(field, name)] as const;
         }),
       ),
       // Override the amount to make it a smaller integer
@@ -117,7 +124,10 @@ function tableArbitrary(tableSchema, extraArbs, requiredKeys = []) {
   return arb;
 }
 
-export function makeTransaction({ splitFreq = 1, payeeIds } = {}) {
+export function makeTransaction({
+  splitFreq = 1,
+  payeeIds,
+}: { splitFreq?: number; payeeIds?: string[] } = {}) {
   let payeeField = payeeIds
     ? { payee: fc.oneof(...payeeIds.map(id => fc.constant(id))) }
     : null;
@@ -137,7 +147,9 @@ export function makeTransaction({ splitFreq = 1, payeeIds } = {}) {
   );
 }
 
-export const makeTransactionArray = (options = {}) => {
+export const makeTransactionArray = (
+  options: { minLength?; maxLength?; splitFreq?; payeeIds? } = {},
+) => {
   let { minLength, maxLength, ...transOpts } = options;
   return fc
     .array(makeTransaction(transOpts), { minLength, maxLength })
diff --git a/packages/loot-core/src/mocks/budget.js b/packages/loot-core/src/mocks/budget.ts
similarity index 98%
rename from packages/loot-core/src/mocks/budget.js
rename to packages/loot-core/src/mocks/budget.ts
index bc387d940..b90ca9958 100644
--- a/packages/loot-core/src/mocks/budget.js
+++ b/packages/loot-core/src/mocks/budget.ts
@@ -9,6 +9,12 @@ import * as sheet from '../server/sheet';
 import { batchMessages, setSyncingMode } from '../server/sync';
 import * as monthUtils from '../shared/months';
 import q from '../shared/query';
+import type {
+  AccountEntity,
+  CategoryGroupEntity,
+  PayeeEntity,
+  TransactionEntity,
+} from '../types/models';
 
 function pickRandom(list) {
   return list[Math.floor(Math.random() * list.length) % list.length];
@@ -100,7 +106,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
       amount = integer(0, Math.random() < 0.05 ? -8000 : -700);
     }
 
-    let transaction = {
+    let transaction: TransactionEntity = {
       amount,
       payee: payee.id,
       account: account.id,
@@ -122,7 +128,7 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
           amount: transaction.amount - a * 2,
           category: pick(),
         },
-      ];
+      ] as TransactionEntity[];
     }
   }
 
@@ -384,7 +390,7 @@ async function fillOther(handlers, account, payees, groups) {
   let numTransactions = integer(3, 6);
   let category = incomeGroup.categories.find(c => c.name === 'Income');
 
-  let transactions = [
+  let transactions: TransactionEntity[] = [
     {
       amount: integer(3250, 3700) * 100 * 100,
       payee: payees.find(p => p.name === 'Starting Balance').id,
@@ -557,7 +563,7 @@ export async function createTestBudget(handlers) {
   await db.runQuery('DELETE FROM categories;');
   await db.runQuery('DELETE FROM category_groups');
 
-  let accounts = [
+  let accounts: AccountEntity[] = [
     { name: 'Bank of America', type: 'checking' },
     { name: 'Ally Savings', type: 'savings' },
     { name: 'Capital One Checking', type: 'checking' },
@@ -575,7 +581,7 @@ export async function createTestBudget(handlers) {
     }),
   );
 
-  let payees = [
+  let payees: PayeeEntity[] = [
     { name: 'Starting Balance' },
     { name: 'Kroger' },
     { name: 'Publix' },
@@ -598,7 +604,7 @@ export async function createTestBudget(handlers) {
     }),
   );
 
-  let categoryGroups = [
+  let categoryGroups: CategoryGroupEntity[] = [
     {
       name: 'Usual Expenses',
       categories: [
@@ -640,6 +646,7 @@ export async function createTestBudget(handlers) {
         isIncome: group.is_income ? 1 : 0,
       });
 
+      // @ts-expect-error Missing proper type refinement
       for (let category of group.categories) {
         category.id = await handlers['category-create']({
           ...category,
diff --git a/packages/loot-core/src/mocks/index.js b/packages/loot-core/src/mocks/index.ts
similarity index 94%
rename from packages/loot-core/src/mocks/index.js
rename to packages/loot-core/src/mocks/index.ts
index b61a8a3b4..f0e055c35 100644
--- a/packages/loot-core/src/mocks/index.js
+++ b/packages/loot-core/src/mocks/index.ts
@@ -1,5 +1,6 @@
 import * as uuid from '../platform/uuid';
 import * as monthUtils from '../shared/months';
+import type { TransactionEntity } from '../types/models';
 
 export function generateAccount(name, isConnected, type, offbudget) {
   return {
@@ -49,7 +50,7 @@ export function generateCategoryGroups(definition) {
   });
 }
 
-function _generateTransaction(data) {
+function _generateTransaction(data): TransactionEntity {
   const id = data.id || uuid.v4Sync();
   return {
     id: id,
@@ -65,7 +66,7 @@ function _generateTransaction(data) {
   };
 }
 
-export function generateTransaction(data, splitAmount, showError = false) {
+export function generateTransaction(data, splitAmount?, showError = false) {
   const result = [];
 
   const trans = _generateTransaction(data);
diff --git a/packages/loot-core/src/mocks/plaid.js b/packages/loot-core/src/mocks/plaid.ts
similarity index 100%
rename from packages/loot-core/src/mocks/plaid.js
rename to packages/loot-core/src/mocks/plaid.ts
diff --git a/packages/loot-core/src/mocks/redux.js b/packages/loot-core/src/mocks/redux.tsx
similarity index 100%
rename from packages/loot-core/src/mocks/redux.js
rename to packages/loot-core/src/mocks/redux.tsx
diff --git a/packages/loot-core/src/mocks/setup.js b/packages/loot-core/src/mocks/setup.ts
similarity index 95%
rename from packages/loot-core/src/mocks/setup.js
rename to packages/loot-core/src/mocks/setup.ts
index 1a415914c..7da62ee46 100644
--- a/packages/loot-core/src/mocks/setup.js
+++ b/packages/loot-core/src/mocks/setup.ts
@@ -58,7 +58,7 @@ global.randomId = () => {
 
 global.getDatabaseDump = async function (tables) {
   if (!tables) {
-    const rows = await sqlite.runQuery(
+    const rows = await sqlite.runQuery<{ name }>(
       db.getDatabase(),
       "SELECT name FROM sqlite_master WHERE type='table'",
       [],
@@ -114,7 +114,7 @@ global.emptyDatabase = function (avoidUpdate) {
 
     await sqlite.init();
 
-    let memoryDB = new sqlite.openDatabase(path);
+    let memoryDB = await sqlite.openDatabase(path);
     sqlite.execQuery(
       memoryDB,
       nativeFs.readFileSync(__dirname + '/../server/sql/init.sql', 'utf8'),
@@ -145,10 +145,10 @@ afterEach(() => {
     if (sheet.get()) {
       sheet.get().onFinish(() => {
         sheet.unloadSpreadsheet();
-        resolve();
+        resolve(undefined);
       });
     } else {
-      resolve();
+      resolve(undefined);
     }
   });
 });
diff --git a/packages/loot-core/src/mocks/spreadsheet.js b/packages/loot-core/src/mocks/spreadsheet.ts
similarity index 100%
rename from packages/loot-core/src/mocks/spreadsheet.js
rename to packages/loot-core/src/mocks/spreadsheet.ts
diff --git a/packages/loot-core/src/mocks/util.js b/packages/loot-core/src/mocks/util.ts
similarity index 91%
rename from packages/loot-core/src/mocks/util.js
rename to packages/loot-core/src/mocks/util.ts
index 6128df60a..0525babc6 100644
--- a/packages/loot-core/src/mocks/util.js
+++ b/packages/loot-core/src/mocks/util.ts
@@ -2,7 +2,10 @@ import { join, dirname, basename } from 'path';
 
 import snapshotDiff from 'snapshot-diff';
 
-export function expectSnapshotWithDiffer(initialValue, { onlyUpdates } = {}) {
+export function expectSnapshotWithDiffer(
+  initialValue,
+  { onlyUpdates }: { onlyUpdates? } = {},
+) {
   let currentValue = initialValue;
   if (!onlyUpdates) {
     expect(initialValue).toMatchSnapshot();
diff --git a/packages/loot-core/src/platform/client/fetch/__mocks__/index.web.ts b/packages/loot-core/src/platform/client/fetch/__mocks__/index.web.ts
index 5761ed568..40cc73c0d 100644
--- a/packages/loot-core/src/platform/client/fetch/__mocks__/index.web.ts
+++ b/packages/loot-core/src/platform/client/fetch/__mocks__/index.web.ts
@@ -1,7 +1,9 @@
+import type * as T from '..';
+
 let listeners = new Map();
 let serverHandler = null;
 
-export const initServer = handlers => {
+export const initServer: T.InitServer = handlers => {
   serverHandler = msg => {
     let { name, args, catchErrors } = msg;
     if (handlers[name]) {
@@ -20,12 +22,12 @@ export const initServer = handlers => {
   };
 };
 
-export const clearServer = () => {
+export const clearServer: T.ClearServer = () => {
   serverHandler = null;
   listeners = new Map();
 };
 
-export const serverPush = (name, args) => {
+export const serverPush: T.ServerPush = (name, args) => {
   Promise.resolve().then(() => {
     const listens = listeners.get(name);
     if (listens) {
diff --git a/packages/loot-core/src/platform/client/fetch/index.d.ts b/packages/loot-core/src/platform/client/fetch/index.d.ts
index 646890e9b..ae2b1aeb9 100644
--- a/packages/loot-core/src/platform/client/fetch/index.d.ts
+++ b/packages/loot-core/src/platform/client/fetch/index.d.ts
@@ -1,4 +1,5 @@
 import type { Handlers } from '../../../types/handlers';
+import type { ServerEvents } from '../../../types/server-events';
 
 export function init(socketName: string): Promise<unknown>;
 export type Init = typeof init;
@@ -16,8 +17,21 @@ export function sendCatch<K extends keyof Handlers>(
 ): ReturnType<Handlers[K]>;
 export type SendCatch = typeof sendCatch;
 
-export function listen(name: string, cb: () => void): () => void;
+export function listen<K extends keyof ServerEvents>(
+  name: K,
+  cb: (arg: ServerEvents[K]) => void,
+): () => void;
 export type Listen = typeof listen;
 
 export function unlisten(name: string): void;
 export type Unlisten = typeof unlisten;
+
+/** Mock functions */
+export function initServer(handlers: unknown): void;
+export type InitServer = typeof initServer;
+
+export function serverPush(name: string, args: unknown): void;
+export type ServerPush = typeof serverPush;
+
+export function clearServer(): void;
+export type ClearServer = typeof clearServer;
diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts
index 6995ad9f9..4a189f3a8 100644
--- a/packages/loot-core/src/platform/server/asyncStorage/index.d.ts
+++ b/packages/loot-core/src/platform/server/asyncStorage/index.d.ts
@@ -1,4 +1,4 @@
-export function init(): void;
+export function init(opts?: { persist?: boolean }): void;
 export type Init = typeof init;
 
 export function getItem(key: string): Promise<string>;
@@ -10,7 +10,7 @@ export type SetItem = typeof setItem;
 export function removeItem(key: string): void;
 export type RemoveItem = typeof removeItem;
 
-export function multiGet(keys: string[]): Promise<[string, unknown][]>;
+export function multiGet(keys: string[]): Promise<[string, string][]>;
 export type MultiGet = typeof multiGet;
 
 export function multiSet(keyValues: [string, unknown][]): void;
diff --git a/packages/loot-core/src/platform/server/asyncStorage/index.web.ts b/packages/loot-core/src/platform/server/asyncStorage/index.web.ts
index e0bde3120..b81081c71 100644
--- a/packages/loot-core/src/platform/server/asyncStorage/index.web.ts
+++ b/packages/loot-core/src/platform/server/asyncStorage/index.web.ts
@@ -60,7 +60,7 @@ export const multiGet: T.MultiGet = async function (keys) {
 
   let promise = Promise.all(
     keys.map(key => {
-      return new Promise<[string, unknown]>((resolve, reject) => {
+      return new Promise<[string, string]>((resolve, reject) => {
         let req = objectStore.get(key);
         req.onerror = e => reject(e);
         req.onsuccess = e => resolve([key, e.target.result]);
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 8bcebcd84..48eec7ff0 100644
--- a/packages/loot-core/src/platform/server/connection/index.d.ts
+++ b/packages/loot-core/src/platform/server/connection/index.d.ts
@@ -1,16 +1,21 @@
+import type { ServerEvents } from '../../../types/server-events';
+
 export function init(
   channel: Window | string,
   handlers: Record<string, () => void>,
 ): void;
 export type Init = typeof init;
 
-export function send(type: string, args?: unknown): void;
+export function send<K extends keyof ServerEvents>(
+  type: K,
+  args?: ServerEvents[k],
+): void;
 export type Send = typeof send;
 
 export function getEvents(): unknown[];
 export type GetEvents = typeof getEvents;
 
-export function getNumClients(): void;
+export function getNumClients(): number;
 export type GetNumClients = typeof getNumClients;
 
 export function resetEvents(): void;
diff --git a/packages/loot-core/src/platform/server/fs/index.d.ts b/packages/loot-core/src/platform/server/fs/index.d.ts
index aa7a9ce05..d9c1d8a60 100644
--- a/packages/loot-core/src/platform/server/fs/index.d.ts
+++ b/packages/loot-core/src/platform/server/fs/index.d.ts
@@ -47,8 +47,9 @@ export type CopyFile = typeof copyFile;
 
 export function readFile(
   filepath: string,
-  encoding?: 'utf8' | 'binary' | null,
-): Promise<string | Buffer>;
+  encoding: 'binary' | null,
+): Promise<Buffer>;
+export function readFile(filepath: string, encoding?: 'utf8'): Promise<string>;
 export type ReadFile = typeof readFile;
 
 export function writeFile(
diff --git a/packages/loot-core/src/platform/server/fs/index.electron.ts b/packages/loot-core/src/platform/server/fs/index.electron.ts
index 46ad57118..89d042397 100644
--- a/packages/loot-core/src/platform/server/fs/index.electron.ts
+++ b/packages/loot-core/src/platform/server/fs/index.electron.ts
@@ -117,13 +117,18 @@ export const copyFile = (frompath, topath) => {
   });
 };
 
-export const readFile: T.ReadFile = (filepath, encoding = 'utf8') => {
+export const readFile: T.ReadFile = (
+  filepath: string,
+  encoding: 'utf8' | 'binary' | null = 'utf8',
+) => {
   if (encoding === 'binary') {
     // `binary` is not actually a valid encoding, you pass `null` into node if
     // you want a buffer
     encoding = null;
   }
-  return new Promise((resolve, reject) => {
+  // `any` as cannot refine return with two function overrides
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return new Promise<any>((resolve, reject) => {
     fs.readFile(filepath, encoding, (err, data) => {
       if (err) {
         reject(err);
diff --git a/packages/loot-core/src/server/__snapshots__/backups.test.js.snap b/packages/loot-core/src/server/__snapshots__/backups.test.ts.snap
similarity index 100%
rename from packages/loot-core/src/server/__snapshots__/backups.test.js.snap
rename to packages/loot-core/src/server/__snapshots__/backups.test.ts.snap
diff --git a/packages/loot-core/src/server/__snapshots__/main.test.js.snap b/packages/loot-core/src/server/__snapshots__/main.test.ts.snap
similarity index 100%
rename from packages/loot-core/src/server/__snapshots__/main.test.js.snap
rename to packages/loot-core/src/server/__snapshots__/main.test.ts.snap
diff --git a/packages/loot-core/src/server/__snapshots__/sheet.test.js.snap b/packages/loot-core/src/server/__snapshots__/sheet.test.ts.snap
similarity index 100%
rename from packages/loot-core/src/server/__snapshots__/sheet.test.js.snap
rename to packages/loot-core/src/server/__snapshots__/sheet.test.ts.snap
diff --git a/packages/loot-core/src/server/api-models.js b/packages/loot-core/src/server/api-models.ts
similarity index 98%
rename from packages/loot-core/src/server/api-models.js
rename to packages/loot-core/src/server/api-models.ts
index c42701eff..ea1da3d13 100644
--- a/packages/loot-core/src/server/api-models.js
+++ b/packages/loot-core/src/server/api-models.ts
@@ -39,7 +39,7 @@ export const transactionModel = {
   },
 
   fromExternal(transaction) {
-    let result = {};
+    let result: Record<string, unknown> = {};
     if ('id' in transaction) {
       result.id = transaction.id;
     }
diff --git a/packages/loot-core/src/server/api.js b/packages/loot-core/src/server/api.ts
similarity index 98%
rename from packages/loot-core/src/server/api.js
rename to packages/loot-core/src/server/api.ts
index 3509ffd2e..4c73f5419 100644
--- a/packages/loot-core/src/server/api.js
+++ b/packages/loot-core/src/server/api.ts
@@ -33,7 +33,7 @@ import { setSyncingMode, batchMessages } from './sync';
 let IMPORT_MODE = false;
 
 // This is duplicate from main.js...
-function APIError(msg, meta) {
+function APIError(msg, meta?) {
   return { type: 'APIError', message: msg, meta };
 }
 
@@ -193,7 +193,7 @@ handlers['api/download-budget'] = async function ({ syncId, password }) {
 
     let result = await handlers['download-budget']({ fileId: file.fileId });
     if (result.error) {
-      throw new Error(getDownloadError(result.error, result.id));
+      throw new Error(getDownloadError(result.error));
     }
     await handlers['load-budget']({ id: result.id });
   }
@@ -466,7 +466,9 @@ handlers['api/account-delete'] = withMutation(async function ({ id }) {
   return handlers['account-close']({ id, forced: true });
 });
 
-handlers['api/categories-get'] = async function ({ grouped } = {}) {
+handlers['api/categories-get'] = async function ({
+  grouped,
+}: { grouped? } = {}) {
   let result = await handlers['get-categories']();
   return grouped
     ? result.grouped.map(categoryGroupModel.toExternal)
diff --git a/packages/loot-core/src/server/app.js b/packages/loot-core/src/server/app.ts
similarity index 96%
rename from packages/loot-core/src/server/app.js
rename to packages/loot-core/src/server/app.ts
index d34b9b09a..c2bca043a 100644
--- a/packages/loot-core/src/server/app.js
+++ b/packages/loot-core/src/server/app.ts
@@ -8,6 +8,11 @@ import { captureException } from '../platform/exceptions';
 // methods an "app".
 
 class App {
+  events;
+  handlers;
+  services;
+  unlistenServices;
+
   constructor() {
     this.handlers = {};
     this.services = [];
diff --git a/packages/loot-core/src/server/backups.test.js b/packages/loot-core/src/server/backups.test.ts
similarity index 100%
rename from packages/loot-core/src/server/backups.test.js
rename to packages/loot-core/src/server/backups.test.ts
diff --git a/packages/loot-core/src/server/backups.js b/packages/loot-core/src/server/backups.ts
similarity index 99%
rename from packages/loot-core/src/server/backups.js
rename to packages/loot-core/src/server/backups.ts
index ca72eaf5c..0766ee670 100644
--- a/packages/loot-core/src/server/backups.js
+++ b/packages/loot-core/src/server/backups.ts
@@ -66,12 +66,10 @@ export async function getAvailableBackups(id) {
     backups.unshift(latestBackup);
   }
 
-  backups = backups.map(backup => ({
+  return backups.map(backup => ({
     ...backup,
     date: backup.date ? dateFns.format(backup.date, 'yyyy-MM-dd h:mm') : null,
   }));
-
-  return backups;
 }
 
 export async function updateBackups(backups) {
diff --git a/packages/loot-core/src/server/bench.js b/packages/loot-core/src/server/bench.ts
similarity index 86%
rename from packages/loot-core/src/server/bench.js
rename to packages/loot-core/src/server/bench.ts
index c0be1eb6e..cb50c267e 100644
--- a/packages/loot-core/src/server/bench.js
+++ b/packages/loot-core/src/server/bench.ts
@@ -8,8 +8,8 @@ const queries = fs
   .split('___BOUNDARY')
   .map(q => q.trim());
 
-function runQueries(n) {
-  for (var i = 0; i < queries.length; i++) {
+function runQueries(n?) {
+  for (let i = 0; i < queries.length; i++) {
     if (queries[i] !== '') {
       db.runQuery(queries[i], [], true);
     }
diff --git a/packages/loot-core/src/server/cloud-storage.js b/packages/loot-core/src/server/cloud-storage.ts
similarity index 98%
rename from packages/loot-core/src/server/cloud-storage.js
rename to packages/loot-core/src/server/cloud-storage.ts
index 1963eeab2..12dafc4bd 100644
--- a/packages/loot-core/src/server/cloud-storage.js
+++ b/packages/loot-core/src/server/cloud-storage.ts
@@ -31,7 +31,7 @@ async function checkHTTPStatus(res) {
   }
 }
 
-async function fetchJSON(...args) {
+async function fetchJSON(...args: Parameters<typeof fetch>) {
   let res = await fetch(...args);
   res = await checkHTTPStatus(res);
   return res.json();
@@ -279,7 +279,7 @@ export async function upload() {
     console.log('Upload failure', err);
 
     if (err instanceof PostError) {
-      throw new FileUploadError(
+      throw FileUploadError(
         err.reason === 'unauthorized'
           ? 'unauthorized'
           : err.reason || 'network',
@@ -362,7 +362,7 @@ export async function listRemoteFiles() {
   }));
 }
 
-export async function download(fileId, replace) {
+export async function download(fileId) {
   let userToken = await asyncStorage.getItem('user-token');
 
   let buffer;
@@ -415,5 +415,5 @@ export async function download(fileId, replace) {
     }
   }
 
-  return importBuffer(fileData, buffer, replace);
+  return importBuffer(fileData, buffer);
 }
diff --git a/packages/loot-core/src/server/db/index.ts b/packages/loot-core/src/server/db/index.ts
index 46eef2310..1d08c3d16 100644
--- a/packages/loot-core/src/server/db/index.ts
+++ b/packages/loot-core/src/server/db/index.ts
@@ -41,7 +41,7 @@ export function getDatabasePath() {
   return dbPath;
 }
 
-export async function openDatabase(id) {
+export async function openDatabase(id?) {
   if (db) {
     await sqlite.closeDatabase(db);
   }
@@ -634,8 +634,8 @@ export async function getTransactionsByDate(
   throw new Error('`getTransactionsByDate` is deprecated');
 }
 
-export async function getTransactions(accountId, arg2?: unknown) {
-  if (arg2 !== undefined) {
+export async function getTransactions(accountId) {
+  if (arguments.length > 1) {
     throw new Error(
       '`getTransactions` was given a second argument, it now only takes a single argument `accountId`',
     );
diff --git a/packages/loot-core/src/server/encryption-internals.js b/packages/loot-core/src/server/encryption-internals.ts
similarity index 85%
rename from packages/loot-core/src/server/encryption-internals.js
rename to packages/loot-core/src/server/encryption-internals.ts
index 79ff20538..02695f529 100644
--- a/packages/loot-core/src/server/encryption-internals.js
+++ b/packages/loot-core/src/server/encryption-internals.ts
@@ -1,6 +1,6 @@
 import crypto from 'crypto';
 
-let ENCRYPTION_ALGORITHM = 'aes-256-gcm';
+let ENCRYPTION_ALGORITHM = 'aes-256-gcm' as const;
 
 export async function sha256String(str) {
   return crypto.createHash('sha256').update(str).digest('base64');
@@ -65,11 +65,16 @@ export function importKey(str) {
  * Generates a Buffer of a desired byte length to be used as either an encryption key or an initialization vector.
  *
  * @private
- * @param {Integer} [numBytes = 32] - Optional, number of bytes to fill the Buffer with.
- * @param {String} [secret = <random bytes>] - Optional, a secret to use as a basis for the key generation algorithm.
- * @returns {Buffer}
  */
-function createKeyBuffer({ numBytes, secret, salt }) {
+function createKeyBuffer({
+  numBytes,
+  secret,
+  salt,
+}: {
+  numBytes?: number;
+  secret?: string;
+  salt?: string;
+}) {
   return crypto.pbkdf2Sync(
     secret || crypto.randomBytes(128).toString('base64'),
     salt || crypto.randomBytes(32).toString('base64'),
diff --git a/packages/loot-core/src/server/encryption-internals.web.js b/packages/loot-core/src/server/encryption-internals.web.ts
similarity index 95%
rename from packages/loot-core/src/server/encryption-internals.web.js
rename to packages/loot-core/src/server/encryption-internals.web.ts
index d808d13a2..9a4161ef9 100644
--- a/packages/loot-core/src/server/encryption-internals.web.js
+++ b/packages/loot-core/src/server/encryption-internals.web.ts
@@ -10,6 +10,7 @@ function browserAlgorithmName(name) {
 }
 
 export async function sha256String(str) {
+  // @ts-expect-error TextEncoder might not accept an argument
   let inputBuffer = new TextEncoder('utf-8').encode(str).buffer;
   let buffer = await crypto.subtle.digest('sha-256', inputBuffer);
   let outputStr = Array.from(new Uint8Array(buffer))
@@ -47,6 +48,7 @@ export async function encrypt(masterKey, value) {
       keyId: masterKey.getId(),
       algorithm: ENCRYPTION_ALGORITHM,
       iv: Buffer.from(iv).toString('base64'),
+      // @ts-expect-error base64 argument is valid only on NodeJS
       authTag: authTag.toString('base64'),
     },
   };
diff --git a/packages/loot-core/src/server/encryption.test.js b/packages/loot-core/src/server/encryption.test.ts
similarity index 100%
rename from packages/loot-core/src/server/encryption.test.js
rename to packages/loot-core/src/server/encryption.test.ts
diff --git a/packages/loot-core/src/server/encryption.js b/packages/loot-core/src/server/encryption.ts
similarity index 99%
rename from packages/loot-core/src/server/encryption.js
rename to packages/loot-core/src/server/encryption.ts
index c42f5c20e..159421aa4 100644
--- a/packages/loot-core/src/server/encryption.js
+++ b/packages/loot-core/src/server/encryption.ts
@@ -7,6 +7,9 @@ import * as internals from './encryption-internals';
 let keys = {};
 
 class Key {
+  id;
+  value;
+
   constructor({ id }) {
     this.id = id || uuid.v4Sync();
   }
diff --git a/packages/loot-core/src/server/errors.js b/packages/loot-core/src/server/errors.ts
similarity index 68%
rename from packages/loot-core/src/server/errors.js
rename to packages/loot-core/src/server/errors.ts
index 418c2df77..3d1ddccc5 100644
--- a/packages/loot-core/src/server/errors.js
+++ b/packages/loot-core/src/server/errors.ts
@@ -1,6 +1,10 @@
 // TODO: normalize error types
 export class PostError extends Error {
-  constructor(reason, meta) {
+  meta;
+  reason;
+  type;
+
+  constructor(reason, meta?) {
     super('PostError: ' + reason);
     this.type = 'PostError';
     this.reason = reason;
@@ -9,6 +13,9 @@ export class PostError extends Error {
 }
 
 export class HTTPError extends Error {
+  statusCode;
+  responseBody;
+
   constructor(code, body) {
     super(`HTTPError: unsuccessful status code (${code}): ${body}`);
     this.statusCode = code;
@@ -17,36 +24,36 @@ export class HTTPError extends Error {
 }
 
 export class SyncError extends Error {
-  constructor(reason, meta) {
+  meta;
+  reason;
+
+  constructor(reason, meta?) {
     super('SyncError: ' + reason);
     this.reason = reason;
     this.meta = meta;
   }
 }
 
-export class TransactionError extends Error {
-  // eslint-disable-next-line no-useless-constructor
-  constructor(message) {
-    super(message);
-  }
-}
+export class TransactionError extends Error {}
 
 export class RuleError extends Error {
-  constructor(type, message) {
+  type;
+
+  constructor(name, message) {
     super('RuleError: ' + message);
-    this.type = type;
+    this.type = name;
   }
 }
 
-export function APIError(msg, meta) {
+export function APIError(msg, meta?) {
   return { type: 'APIError', message: msg, meta };
 }
 
-export function FileDownloadError(reason, meta) {
+export function FileDownloadError(reason, meta?) {
   return { type: 'FileDownloadError', reason, meta };
 }
 
-export function FileUploadError(reason, meta) {
+export function FileUploadError(reason, meta?) {
   return { type: 'FileUploadError', reason, meta };
 }
 
diff --git a/packages/loot-core/src/server/main-app.js b/packages/loot-core/src/server/main-app.ts
similarity index 100%
rename from packages/loot-core/src/server/main-app.js
rename to packages/loot-core/src/server/main-app.ts
diff --git a/packages/loot-core/src/server/main.test.js b/packages/loot-core/src/server/main.test.ts
similarity index 99%
rename from packages/loot-core/src/server/main.test.js
rename to packages/loot-core/src/server/main.test.ts
index 5fd901dd7..7aa48c5d8 100644
--- a/packages/loot-core/src/server/main.test.js
+++ b/packages/loot-core/src/server/main.test.ts
@@ -29,6 +29,7 @@ afterEach(async () => {
   await runHandler(handlers['close-budget']);
   connection.resetEvents();
   enableGlobalMutations();
+  global.currentMonth = null;
 });
 
 async function createTestBudget(name) {
@@ -234,7 +235,7 @@ describe('Budget', () => {
 
     // Fast-forward in time to a future month and make sure it creates
     // budgets for the months in the future
-    monthUtils.currentMonth = () => '2017-02';
+    global.currentMonth = '2017-02';
 
     bounds = await runHandler(handlers['get-budget-bounds']);
     expect(bounds.start).toBe('2016-02');
@@ -247,7 +248,7 @@ describe('Budget', () => {
   test('budget updates when changing a category', async () => {
     const spreadsheet = await sheet.loadSpreadsheet(db);
     function captureChangedCells(func) {
-      return new Promise(async resolve => {
+      return new Promise<unknown[]>(async resolve => {
         let changed = [];
         let remove = spreadsheet.addEventListener('change', ({ names }) => {
           changed = changed.concat(names);
diff --git a/packages/loot-core/src/server/main.js b/packages/loot-core/src/server/main.ts
similarity index 98%
rename from packages/loot-core/src/server/main.js
rename to packages/loot-core/src/server/main.ts
index e22068a5f..17a0f752c 100644
--- a/packages/loot-core/src/server/main.js
+++ b/packages/loot-core/src/server/main.ts
@@ -140,23 +140,6 @@ handlers['transaction-delete'] = mutator(async function (transaction) {
   return {};
 });
 
-handlers['transactions-filter'] = async function ({
-  term,
-  accountId,
-  latestDate,
-  count,
-  notPaged,
-  options = {},
-}) {
-  return db.getTransactions(
-    term,
-    accountId,
-    latestDate,
-    notPaged ? null : count == null ? undefined : count,
-    options,
-  );
-};
-
 handlers['transactions-parse-file'] = async function ({ filepath, options }) {
   return parseFile(filepath, options);
 };
@@ -1120,7 +1103,7 @@ handlers['accounts-sync'] = async function ({ id }) {
   let matchedTransactions = [];
   let updatedAccounts = [];
 
-  for (var i = 0; i < accounts.length; i++) {
+  for (let i = 0; i < accounts.length; i++) {
     const acct = accounts[i];
     if (acct.bankId) {
       try {
@@ -1322,7 +1305,7 @@ handlers['nordigen-accounts-sync'] = async function ({ id }) {
   let matchedTransactions = [];
   let updatedAccounts = [];
 
-  for (var i = 0; i < accounts.length; i++) {
+  for (let i = 0; i < accounts.length; i++) {
     const acct = accounts[i];
     if (acct.bankId) {
       try {
@@ -1635,7 +1618,9 @@ handlers['key-test'] = async function ({ fileId, password }) {
   return {};
 };
 
-handlers['subscribe-needs-bootstrap'] = async function ({ url } = {}) {
+handlers['subscribe-needs-bootstrap'] = async function ({
+  url,
+}: { url? } = {}) {
   if (getServer(url).BASE_SERVER === UNCONFIGURED_SERVER) {
     return { bootstrapped: true, hasServer: false };
   }
@@ -1688,15 +1673,15 @@ handlers['subscribe-get-user'] = async function () {
 
   if (userToken) {
     try {
-      let res = await get(getServer().SIGNUP_SERVER + '/validate', {
+      const res = await get(getServer().SIGNUP_SERVER + '/validate', {
         headers: {
           'X-ACTUAL-TOKEN': userToken,
         },
       });
-      res = JSON.parse(res);
+      const { status, reason } = JSON.parse(res);
 
-      if (res.status === 'error') {
-        if (res.reason === 'unauthorized') {
+      if (status === 'error') {
+        if (reason === 'unauthorized') {
           return null;
         }
         return { offline: true };
@@ -1845,7 +1830,7 @@ handlers['reset-budget-cache'] = mutator(async function () {
   await sheet.waitOnSpreadsheet();
 });
 
-handlers['upload-budget'] = async function ({ id } = {}) {
+handlers['upload-budget'] = async function ({ id }: { id? } = {}) {
   if (id) {
     if (prefs.getPrefs()) {
       throw new Error('upload-budget: id given but prefs already loaded');
@@ -1872,10 +1857,10 @@ handlers['upload-budget'] = async function ({ id } = {}) {
   return {};
 };
 
-handlers['download-budget'] = async function ({ fileId, replace }) {
+handlers['download-budget'] = async function ({ fileId }) {
   let result;
   try {
-    result = await cloudStorage.download(fileId, replace);
+    result = await cloudStorage.download(fileId);
   } catch (e) {
     if (e.type === 'FileDownloadError') {
       if (e.reason === 'file-exists' && e.meta.id) {
@@ -1924,7 +1909,7 @@ handlers['load-budget'] = async function ({ id }) {
     }
   }
 
-  let res = await loadBudget(id, { showUpdate: true });
+  let res = await loadBudget(id);
 
   return res;
 };
@@ -1988,6 +1973,11 @@ handlers['create-budget'] = async function ({
   avoidUpload,
   testMode,
   testBudgetId,
+}: {
+  budgetName?;
+  avoidUpload?;
+  testMode?;
+  testBudgetId?;
 } = {}) {
   let id;
   if (testMode) {
@@ -2143,7 +2133,7 @@ handlers['export-budget'] = async function () {
   return await cloudStorage.exportBuffer();
 };
 
-async function loadBudget(id, { showUpdate } = {}) {
+async function loadBudget(id) {
   let dir;
   try {
     dir = fs.getBudgetDir(id);
@@ -2178,10 +2168,8 @@ async function loadBudget(id, { showUpdate } = {}) {
     prefs.savePrefs({ userId });
   }
 
-  let { budgetVersion } = prefs.getPrefs();
-
   try {
-    await updateVersion(budgetVersion, showUpdate);
+    await updateVersion();
   } catch (e) {
     console.warn('Error updating', e);
     let result;
diff --git a/packages/loot-core/src/server/models.js b/packages/loot-core/src/server/models.ts
similarity index 92%
rename from packages/loot-core/src/server/models.js
rename to packages/loot-core/src/server/models.ts
index 1f504569b..86bcd3ed5 100644
--- a/packages/loot-core/src/server/models.js
+++ b/packages/loot-core/src/server/models.ts
@@ -51,7 +51,7 @@ export const accountModel = {
     }
   },
 
-  validate(account, { update } = {}) {
+  validate(account, { update }: { update?: boolean } = {}) {
     if (!update || account.type != null) {
       accountModel.validateAccountType(account);
     }
@@ -68,7 +68,7 @@ export const accountModel = {
 };
 
 export const categoryModel = {
-  validate(category, { update } = {}) {
+  validate(category, { update }: { update?: boolean } = {}) {
     requiredFields(
       'category',
       category,
@@ -82,7 +82,7 @@ export const categoryModel = {
 };
 
 export const categoryGroupModel = {
-  validate(categoryGroup, { update } = {}) {
+  validate(categoryGroup, { update }: { update?: boolean } = {}) {
     requiredFields(
       'categoryGroup',
       categoryGroup,
@@ -96,7 +96,7 @@ export const categoryGroupModel = {
 };
 
 export const payeeModel = {
-  validate(payee, { update } = {}) {
+  validate(payee, { update }: { update?: boolean } = {}) {
     requiredFields('payee', payee, ['name'], update);
     return payee;
   },
@@ -110,7 +110,7 @@ export const payeeRuleModel = {
     }
   },
 
-  validate(rule, { update } = {}) {
+  validate(rule, { update }: { update?: boolean } = {}) {
     if (!update || 'type' in rule) {
       payeeRuleModel.validateType(rule);
     }
@@ -121,7 +121,7 @@ export const payeeRuleModel = {
 };
 
 export const transactionModel = {
-  validate(trans, { update } = {}) {
+  validate(trans, { update }: { update?: boolean } = {}) {
     requiredFields('transaction', trans, ['date', 'acct'], update);
 
     if ('date' in trans) {
diff --git a/packages/loot-core/src/server/mutators.js b/packages/loot-core/src/server/mutators.ts
similarity index 90%
rename from packages/loot-core/src/server/mutators.js
rename to packages/loot-core/src/server/mutators.ts
index 0cb518cfd..a50472c63 100644
--- a/packages/loot-core/src/server/mutators.js
+++ b/packages/loot-core/src/server/mutators.ts
@@ -36,7 +36,11 @@ function wait(time) {
   return new Promise(resolve => setTimeout(resolve, time));
 }
 
-export async function runHandler(handler, args, { undoTag, name } = {}) {
+export async function runHandler(
+  handler,
+  args?,
+  { undoTag, name }: { undoTag?; name? } = {},
+) {
   // For debug reasons, track the latest handlers that have been
   // called
   _latestHandlerNames.push(name);
@@ -77,12 +81,17 @@ export function disableGlobalMutations() {
   }
 }
 
-export const runMutator = sequential(async (func, initialContext = {}) => {
+function _runMutator<T extends () => Promise<unknown>>(
+  func: T,
+  initialContext = {},
+) {
   currentContext = initialContext;
   return func().finally(() => {
     currentContext = null;
-  });
-});
+  }) as ReturnType<T>;
+}
+// Type cast needed as TS looses types over nested generic returns
+export const runMutator = sequential(_runMutator) as typeof _runMutator;
 
 export function withMutatorContext(context, func) {
   if (currentContext == null && !globalMutationsEnabled) {
diff --git a/packages/loot-core/src/server/perf.js b/packages/loot-core/src/server/perf.ts
similarity index 100%
rename from packages/loot-core/src/server/perf.js
rename to packages/loot-core/src/server/perf.ts
diff --git a/packages/loot-core/src/server/platform.js b/packages/loot-core/src/server/platform.ts
similarity index 100%
rename from packages/loot-core/src/server/platform.js
rename to packages/loot-core/src/server/platform.ts
diff --git a/packages/loot-core/src/server/platform.web.js b/packages/loot-core/src/server/platform.web.ts
similarity index 100%
rename from packages/loot-core/src/server/platform.web.js
rename to packages/loot-core/src/server/platform.web.ts
diff --git a/packages/loot-core/src/server/polyfills.js b/packages/loot-core/src/server/polyfills.ts
similarity index 100%
rename from packages/loot-core/src/server/polyfills.js
rename to packages/loot-core/src/server/polyfills.ts
diff --git a/packages/loot-core/src/server/post.ts b/packages/loot-core/src/server/post.ts
index 1f0bbc82d..891965063 100644
--- a/packages/loot-core/src/server/post.ts
+++ b/packages/loot-core/src/server/post.ts
@@ -89,6 +89,6 @@ export async function postBinary(url, data, headers) {
   return buffer;
 }
 
-export function get(url, opts) {
+export function get(url, opts?) {
   return fetch(url, opts).then(res => res.text());
 }
diff --git a/packages/loot-core/src/server/prefs.js b/packages/loot-core/src/server/prefs.ts
similarity index 98%
rename from packages/loot-core/src/server/prefs.js
rename to packages/loot-core/src/server/prefs.ts
index 9cbf7c6b9..43d89db2e 100644
--- a/packages/loot-core/src/server/prefs.js
+++ b/packages/loot-core/src/server/prefs.ts
@@ -5,7 +5,7 @@ import { sendMessages } from './sync';
 
 let prefs = null;
 
-export async function loadPrefs(id) {
+export async function loadPrefs(id?) {
   if (process.env.NODE_ENV === 'test' && !id) {
     prefs = { dummyTestPrefs: true };
     return prefs;
diff --git a/packages/loot-core/src/server/server-config.js b/packages/loot-core/src/server/server-config.ts
similarity index 94%
rename from packages/loot-core/src/server/server-config.js
rename to packages/loot-core/src/server/server-config.ts
index c509cdc12..56f62cd10 100644
--- a/packages/loot-core/src/server/server-config.js
+++ b/packages/loot-core/src/server/server-config.ts
@@ -17,7 +17,7 @@ export function setServer(url) {
 }
 
 // `url` is optional; if not given it will provide the global config
-export function getServer(url) {
+export function getServer(url?) {
   if (url) {
     return {
       BASE_SERVER: url,
diff --git a/packages/loot-core/src/server/sheet.test.js b/packages/loot-core/src/server/sheet.test.ts
similarity index 95%
rename from packages/loot-core/src/server/sheet.test.js
rename to packages/loot-core/src/server/sheet.test.ts
index f48a9e741..82c8861d1 100644
--- a/packages/loot-core/src/server/sheet.test.js
+++ b/packages/loot-core/src/server/sheet.test.ts
@@ -54,7 +54,7 @@ describe('Spreadsheet', () => {
     await new Promise(resolve => {
       spreadsheet.onFinish(() => {
         expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
-        resolve();
+        resolve(undefined);
       });
     });
 
@@ -63,7 +63,7 @@ describe('Spreadsheet', () => {
     return new Promise(resolve => {
       spreadsheet.onFinish(() => {
         expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
-        resolve();
+        resolve(undefined);
       });
     });
   });
@@ -84,7 +84,7 @@ describe('Spreadsheet', () => {
     await new Promise(resolve => {
       spreadsheet.onFinish(() => {
         expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
-        resolve();
+        resolve(undefined);
       });
     });
 
@@ -93,7 +93,7 @@ describe('Spreadsheet', () => {
     await new Promise(resolve => {
       spreadsheet.onFinish(() => {
         expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
-        resolve();
+        resolve(undefined);
       });
     });
   });
diff --git a/packages/loot-core/src/server/sheet.js b/packages/loot-core/src/server/sheet.ts
similarity index 95%
rename from packages/loot-core/src/server/sheet.js
rename to packages/loot-core/src/server/sheet.ts
index 199fd8277..8eb5341c0 100644
--- a/packages/loot-core/src/server/sheet.js
+++ b/packages/loot-core/src/server/sheet.ts
@@ -54,7 +54,7 @@ function setCacheStatus(mainDb, cacheDb, { clean }) {
 }
 
 function isCacheDirty(mainDb, cacheDb) {
-  let rows = sqlite.runQuery(
+  let rows = sqlite.runQuery<{ key?: number }>(
     cacheDb,
     'SELECT key FROM kvcache_key WHERE id = 1',
     [],
@@ -67,7 +67,7 @@ function isCacheDirty(mainDb, cacheDb) {
   }
 
   if (mainDb) {
-    let rows = sqlite.runQuery(
+    let rows = sqlite.runQuery<{ key?: number }>(
       mainDb,
       'SELECT key FROM kvcache_key WHERE id = 1',
       [],
@@ -84,7 +84,7 @@ function isCacheDirty(mainDb, cacheDb) {
   return rows.length === 0;
 }
 
-export async function loadSpreadsheet(db, onSheetChange) {
+export async function loadSpreadsheet(db, onSheetChange?) {
   let cacheEnabled = process.env.NODE_ENV !== 'test';
   let mainDb = db.getDatabase();
   let cacheDb;
@@ -132,7 +132,7 @@ export async function loadSpreadsheet(db, onSheetChange) {
   }
 
   if (cacheEnabled && !isCacheDirty(mainDb, cacheDb)) {
-    let cachedRows = await sqlite.runQuery(
+    let cachedRows = await sqlite.runQuery<{ key?: number; value: string }>(
       cacheDb,
       'SELECT * FROM kvcache',
       [],
@@ -243,7 +243,7 @@ export function waitOnSpreadsheet() {
     if (globalSheet) {
       globalSheet.onFinish(resolve);
     } else {
-      resolve();
+      resolve(undefined);
     }
   });
 }
diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts
index 104a6b97e..6aab81a77 100644
--- a/packages/loot-core/src/server/sync/index.ts
+++ b/packages/loot-core/src/server/sync/index.ts
@@ -249,7 +249,8 @@ type Message = {
 
 export const applyMessages = sequential(async (messages: Message[]) => {
   if (checkSyncingMode('import')) {
-    return applyMessagesForImport(messages);
+    applyMessagesForImport(messages);
+    return undefined;
   } else if (checkSyncingMode('enabled')) {
     // Compare the messages with the existing crdt. This filters out
     // already applied messages and determines if a message is old or
@@ -671,7 +672,7 @@ async function _fullSync(sinceTimestamp, count, prevDiffTime) {
   let localTimeChanged = getClock().timestamp.toString() !== currentTime;
 
   // Apply the new messages
-  let receivedMessages: unknown[] = [];
+  let receivedMessages: Message[] = [];
   if (res.messages.length > 0) {
     receivedMessages = await receiveMessages(
       res.messages.map(msg => ({
diff --git a/packages/loot-core/src/server/sync/reset.ts b/packages/loot-core/src/server/sync/reset.ts
index 4f21dc0cd..27e18feb9 100644
--- a/packages/loot-core/src/server/sync/reset.ts
+++ b/packages/loot-core/src/server/sync/reset.ts
@@ -6,7 +6,7 @@ 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?) {
   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/undo.js b/packages/loot-core/src/server/undo.ts
similarity index 92%
rename from packages/loot-core/src/server/undo.js
rename to packages/loot-core/src/server/undo.ts
index f6ecdf7e1..6217ea6be 100644
--- a/packages/loot-core/src/server/undo.js
+++ b/packages/loot-core/src/server/undo.ts
@@ -6,7 +6,17 @@ import { withMutatorContext, getMutatorContext } from './mutators';
 import { sendMessages } from './sync';
 
 // A marker always sits as the first entry to simplify logic
-let MESSAGE_HISTORY = [{ type: 'marker' }];
+type MarkerMessage = { type: 'marker'; meta? };
+type MessagesMessage = {
+  type: 'messages';
+  messages: unknown[];
+  meta?;
+  oldData;
+  undoTag;
+};
+let MESSAGE_HISTORY: Array<MarkerMessage | MessagesMessage> = [
+  { type: 'marker' },
+];
 let CURSOR = 0;
 let HISTORY_SIZE = 20;
 
@@ -45,7 +55,7 @@ export function clearUndo() {
   CURSOR = 0;
 }
 
-export function withUndo(func, meta) {
+export function withUndo(func, meta?) {
   let context = getMutatorContext();
   if (context.undoDisabled || context.undoListening) {
     return func();
@@ -53,7 +63,7 @@ export function withUndo(func, meta) {
 
   MESSAGE_HISTORY = MESSAGE_HISTORY.slice(0, CURSOR + 1);
 
-  let marker = { type: 'marker', meta };
+  let marker = { type: 'marker' as const, meta };
 
   if (MESSAGE_HISTORY[MESSAGE_HISTORY.length - 1].type === 'marker') {
     MESSAGE_HISTORY[MESSAGE_HISTORY.length - 1] = marker;
@@ -110,7 +120,7 @@ export async function undo() {
   let meta = MESSAGE_HISTORY[CURSOR].meta;
   let start = Math.max(CURSOR, 0);
   let entries = MESSAGE_HISTORY.slice(start, end + 1).filter(
-    entry => entry.type === 'messages',
+    (entry): entry is MessagesMessage => entry.type === 'messages',
   );
 
   if (entries.length > 0) {
@@ -192,7 +202,7 @@ export async function redo() {
 
   let end = CURSOR;
   let entries = MESSAGE_HISTORY.slice(start + 1, end + 1).filter(
-    entry => entry.type === 'messages',
+    (entry): entry is MessagesMessage => entry.type === 'messages',
   );
 
   if (entries.length > 0) {
@@ -207,7 +217,7 @@ export async function redo() {
 }
 
 function redoResurrections(messages, oldData) {
-  let resurrect = new Set();
+  let resurrect = new Set<string>();
 
   messages.forEach(message => {
     // If any of the ids didn't exist before, we need to "resurrect"
diff --git a/packages/loot-core/src/server/update.js b/packages/loot-core/src/server/update.ts
similarity index 100%
rename from packages/loot-core/src/server/update.js
rename to packages/loot-core/src/server/update.ts
diff --git a/packages/loot-core/src/shared/months.ts b/packages/loot-core/src/shared/months.ts
index 5132c0dcc..7a48d7be4 100644
--- a/packages/loot-core/src/shared/months.ts
+++ b/packages/loot-core/src/shared/months.ts
@@ -85,7 +85,7 @@ export function dayFromDate(date) {
 
 export function currentMonth() {
   if (global.IS_TESTING) {
-    return '2017-01';
+    return global.currentMonth || '2017-01';
   } else {
     return d.format(new Date(), 'yyyy-MM');
   }
diff --git a/packages/loot-core/src/shared/test-helpers.ts b/packages/loot-core/src/shared/test-helpers.ts
index 6c3d499f7..574d92599 100644
--- a/packages/loot-core/src/shared/test-helpers.ts
+++ b/packages/loot-core/src/shared/test-helpers.ts
@@ -14,7 +14,7 @@ function timeout(promise, n) {
   ]);
 }
 
-export function resetTracer(x) {
+export function resetTracer() {
   tracer = execTracer();
 }
 
diff --git a/packages/loot-core/src/shared/transactions.test.js b/packages/loot-core/src/shared/transactions.test.ts
similarity index 100%
rename from packages/loot-core/src/shared/transactions.test.js
rename to packages/loot-core/src/shared/transactions.test.ts
diff --git a/packages/loot-core/src/types/handlers.d.ts b/packages/loot-core/src/types/handlers.d.ts
index e4eb27a09..8ed197b7a 100644
--- a/packages/loot-core/src/types/handlers.d.ts
+++ b/packages/loot-core/src/types/handlers.d.ts
@@ -5,8 +5,9 @@ import type { SchedulesHandlers } from '../server/schedules/types/handlers';
 import type { ApiHandlers } from './api.handlers';
 import type { MainHandlers } from './main.handlers';
 
-export type Handlers = MainHandlers &
-  ApiHandlers &
-  BudgetHandlers &
-  NotesHandlers &
-  SchedulesHandlers;
+export interface Handlers
+  extends MainHandlers,
+    ApiHandlers,
+    BudgetHandlers,
+    NotesHandlers,
+    SchedulesHandlers {}
diff --git a/packages/loot-core/src/types/main.handlers.d.ts b/packages/loot-core/src/types/main.handlers.d.ts
index 51d1065fa..0145d703a 100644
--- a/packages/loot-core/src/types/main.handlers.d.ts
+++ b/packages/loot-core/src/types/main.handlers.d.ts
@@ -20,15 +20,6 @@ export interface MainHandlers {
 
   'transaction-delete': (transaction) => Promise<Record<string, never>>;
 
-  'transactions-filter': (arg: {
-    term;
-    accountId;
-    latestDate;
-    count;
-    notPaged;
-    options;
-  }) => Promise<unknown>;
-
   'transactions-parse-file': (arg: { filepath; options }) => Promise<unknown>;
 
   'transactions-export': (arg: {
@@ -130,7 +121,7 @@ export interface MainHandlers {
 
   'create-query': (arg: { sheetName; name; query }) => Promise<unknown>;
 
-  query: (query) => Promise<unknown>;
+  query: (query) => Promise<{ data; dependencies }>;
 
   'bank-delete': (arg: { id }) => Promise<unknown>;
 
diff --git a/packages/loot-core/src/types/models/account.d.ts b/packages/loot-core/src/types/models/account.d.ts
new file mode 100644
index 000000000..b00123056
--- /dev/null
+++ b/packages/loot-core/src/types/models/account.d.ts
@@ -0,0 +1,11 @@
+export interface AccountEntity {
+  id?: string;
+  name: string;
+  type?: string;
+  offbudget?: boolean;
+  closed?: boolean;
+  sort_order?: number;
+  tombstone?: boolean;
+  // TODO: remove once properly typed
+  [k: string]: unknown;
+}
diff --git a/packages/loot-core/src/types/models/category-group.d.ts b/packages/loot-core/src/types/models/category-group.d.ts
new file mode 100644
index 000000000..227dba003
--- /dev/null
+++ b/packages/loot-core/src/types/models/category-group.d.ts
@@ -0,0 +1,9 @@
+export interface CategoryGroupEntity {
+  id?: string;
+  name: string;
+  is_income?: boolean;
+  sort_order?: number;
+  tombstone?: boolean;
+  // TODO: remove once properly typed
+  [k: string]: unknown;
+}
diff --git a/packages/loot-core/src/types/models/category.d.ts b/packages/loot-core/src/types/models/category.d.ts
new file mode 100644
index 000000000..b495fe0d0
--- /dev/null
+++ b/packages/loot-core/src/types/models/category.d.ts
@@ -0,0 +1,10 @@
+import type { CategoryGroupEntity } from './category-group';
+
+export interface CategoryEntity {
+  id?: string;
+  name: string;
+  is_income?: boolean;
+  group: CategoryGroupEntity;
+  sort_order?: number;
+  tombstone?: boolean;
+}
diff --git a/packages/loot-core/src/types/models/index.d.ts b/packages/loot-core/src/types/models/index.d.ts
new file mode 100644
index 000000000..4c2ba4496
--- /dev/null
+++ b/packages/loot-core/src/types/models/index.d.ts
@@ -0,0 +1,6 @@
+export type * from './account';
+export type * from './category';
+export type * from './category-group';
+export type * from './payee';
+export type * from './schedule';
+export type * from './transaction';
diff --git a/packages/loot-core/src/types/models/payee.d.ts b/packages/loot-core/src/types/models/payee.d.ts
new file mode 100644
index 000000000..67f14cd30
--- /dev/null
+++ b/packages/loot-core/src/types/models/payee.d.ts
@@ -0,0 +1,10 @@
+import type { AccountEntity } from './account';
+
+export interface PayeeEntity {
+  id?: string;
+  name: string;
+  transfer_acct?: AccountEntity;
+  tombstone?: boolean;
+  // TODO: remove once properly typed
+  [k: string]: unknown;
+}
diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts
new file mode 100644
index 000000000..e2991e0cc
--- /dev/null
+++ b/packages/loot-core/src/types/models/rule.d.ts
@@ -0,0 +1,8 @@
+export interface RuleEntity {
+  id: string;
+  stage: string;
+  conditions_op: string;
+  conditions: unknown;
+  actions: unknown;
+  tombstone: boolean;
+}
diff --git a/packages/loot-core/src/types/models/schedule.d.ts b/packages/loot-core/src/types/models/schedule.d.ts
new file mode 100644
index 000000000..f170b736a
--- /dev/null
+++ b/packages/loot-core/src/types/models/schedule.d.ts
@@ -0,0 +1,23 @@
+import type { AccountEntity } from './account';
+import type { PayeeEntity } from './payee';
+import type { RuleEntity } from './rule';
+
+export interface ScheduleEntity {
+  id?: string;
+  name?: string;
+  rule: RuleEntity;
+  next_date: string;
+  completed: boolean;
+  posts_transaction: boolean;
+  tombstone: boolean;
+
+  // These are special fields that are actually pulled from the
+  // underlying rule
+  _payee: PayeeEntity;
+  _account: AccountEntity;
+  _amount: unknown;
+  _amountOp: string;
+  _date: unknown;
+  _conditions: unknown;
+  _actions: unknown;
+}
diff --git a/packages/loot-core/src/types/models/transaction.d.ts b/packages/loot-core/src/types/models/transaction.d.ts
new file mode 100644
index 000000000..b5366b8d6
--- /dev/null
+++ b/packages/loot-core/src/types/models/transaction.d.ts
@@ -0,0 +1,29 @@
+import type { AccountEntity } from './account';
+import type { CategoryEntity } from './category';
+import type { PayeeEntity } from './payee';
+import type { ScheduleEntity } from './schedule';
+
+export interface TransactionEntity {
+  id?: string;
+  is_parent?: boolean;
+  is_child?: boolean;
+  parent_id?: string;
+  account: AccountEntity;
+  category?: CategoryEntity;
+  amount: number;
+  payee?: PayeeEntity;
+  notes?: string;
+  date: string;
+  imported_id?: string;
+  error?: unknown;
+  imported_payee?: string;
+  starting_balance_flag?: boolean;
+  transfer_id?: string;
+  sort_order?: number;
+  cleared?: boolean;
+  tombstone?: boolean;
+  schedule?: ScheduleEntity;
+  subtransactions?: TransactionEntity[];
+  // TODO: remove once properly typed
+  [k: string]: unknown;
+}
diff --git a/packages/loot-core/src/types/server-events.d.ts b/packages/loot-core/src/types/server-events.d.ts
new file mode 100644
index 000000000..75f5c8db7
--- /dev/null
+++ b/packages/loot-core/src/types/server-events.d.ts
@@ -0,0 +1,16 @@
+export interface ServerEvents {
+  'backups-updated': unknown;
+  'cells-changed': Array<{ name }>;
+  'fallback-write-error': unknown;
+  'finish-import': unknown;
+  'finish-load': unknown;
+  'orphaned-payees': unknown;
+  'prefs-updated': unknown;
+  'schedules-offline': { payees: unknown[] };
+  'server-error': unknown;
+  'show-budgets': unknown;
+  'start-import': unknown;
+  'start-load': unknown;
+  'sync-event': { type; subtype; meta; tables };
+  'undo-event': unknown;
+}
diff --git a/packages/loot-core/typings.d.ts b/packages/loot-core/typings/pegjs.d.ts
similarity index 100%
rename from packages/loot-core/typings.d.ts
rename to packages/loot-core/typings/pegjs.d.ts
diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts
new file mode 100644
index 000000000..fc21a3e45
--- /dev/null
+++ b/packages/loot-core/typings/window.d.ts
@@ -0,0 +1,16 @@
+export {};
+
+declare global {
+  // eslint-disable-next-line no-unused-vars
+  interface Window {
+    Actual?: {
+      IS_FAKE_WEB: boolean;
+      ACTUAL_VERSION: string;
+    };
+
+    __history?: {
+      location;
+      push(url: string, opts?: unknown): void;
+    };
+  }
+}
diff --git a/packages/loot-core/webpack/webpack.browser.config.js b/packages/loot-core/webpack/webpack.browser.config.js
index c30427f3b..e8bc0bdae 100644
--- a/packages/loot-core/webpack/webpack.browser.config.js
+++ b/packages/loot-core/webpack/webpack.browser.config.js
@@ -5,7 +5,7 @@ let webpack = require('webpack');
 /** @type {webpack.Configuration} */
 module.exports = {
   mode: process.env.NODE_ENV === 'development' ? 'development' : 'production',
-  entry: path.join(__dirname, '../src/server/main.js'),
+  entry: path.join(__dirname, '../src/server/main.ts'),
   context: path.resolve(__dirname, '../../..'),
   devtool: false,
   output: {
diff --git a/upcoming-release-notes/957.md b/upcoming-release-notes/957.md
new file mode 100644
index 000000000..8c18f3ef8
--- /dev/null
+++ b/upcoming-release-notes/957.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [albertogasparin]
+---
+
+Finish converting `loot-core` to Typescript
-- 
GitLab