From 5cfa2cf577d94a99ddc55b519847fd79547e881b Mon Sep 17 00:00:00 2001
From: Matiss Janis Aboltins <matiss@mja.lv>
Date: Tue, 3 Sep 2024 19:35:02 +0100
Subject: [PATCH] :recycle: (typescript) moving account component to TS (#3311)

---
 packages/desktop-client/package.json          |   1 +
 .../accounts/{Account.jsx => Account.tsx}     | 523 ++++++++++++------
 .../src/components/filters/FiltersStack.tsx   |   2 +-
 .../filters/SavedFilterMenuButton.tsx         |   4 +-
 .../desktop-client/src/hooks/useSelected.tsx  |   6 +-
 .../src/client/data-hooks/filters.ts          |   2 +-
 packages/loot-core/src/client/queries.ts      |   4 +-
 .../src/client/state-types/modals.d.ts        |   6 +-
 .../src/client/state-types/queries.d.ts       |   8 +-
 packages/loot-core/src/types/models/rule.d.ts |   3 +-
 .../src/types/models/transaction-filter.d.ts  |   6 +-
 .../src/types/models/transaction.d.ts         |   2 +
 packages/loot-core/src/types/prefs.d.ts       |   1 +
 upcoming-release-notes/3311.md                |   6 +
 yarn.lock                                     |   8 +
 15 files changed, 405 insertions(+), 177 deletions(-)
 rename packages/desktop-client/src/components/accounts/{Account.jsx => Account.tsx} (77%)
 create mode 100644 upcoming-release-notes/3311.md

diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json
index da2b2e679..112df73b7 100644
--- a/packages/desktop-client/package.json
+++ b/packages/desktop-client/package.json
@@ -20,6 +20,7 @@
     "@swc/plugin-react-remove-properties": "^1.5.121",
     "@testing-library/react": "14.1.2",
     "@testing-library/user-event": "14.5.2",
+    "@types/debounce": "^1.2.4",
     "@types/lodash": "^4",
     "@types/promise-retry": "^1.1.6",
     "@types/react": "^18.2.0",
diff --git a/packages/desktop-client/src/components/accounts/Account.jsx b/packages/desktop-client/src/components/accounts/Account.tsx
similarity index 77%
rename from packages/desktop-client/src/components/accounts/Account.jsx
rename to packages/desktop-client/src/components/accounts/Account.tsx
index ece252ea1..9c5c93877 100644
--- a/packages/desktop-client/src/components/accounts/Account.jsx
+++ b/packages/desktop-client/src/components/accounts/Account.tsx
@@ -1,4 +1,11 @@
-import React, { PureComponent, createRef, useMemo } from 'react';
+// @ts-strict-ignore
+import React, {
+  PureComponent,
+  type MutableRefObject,
+  createRef,
+  useMemo,
+  type ReactElement,
+} from 'react';
 import { Trans, useTranslation } from 'react-i18next';
 import { useSelector } from 'react-redux';
 import { Navigate, useParams, useLocation } from 'react-router-dom';
@@ -8,6 +15,7 @@ import { t } from 'i18next';
 import { v4 as uuidv4 } from 'uuid';
 
 import { validForTransfer } from 'loot-core/client/transfer';
+import { type UndoState } from 'loot-core/server/undo';
 import { useFilters } from 'loot-core/src/client/data-hooks/filters';
 import {
   SchedulesProvider,
@@ -17,7 +25,7 @@ import * as queries from 'loot-core/src/client/queries';
 import { runQuery, pagedQuery } from 'loot-core/src/client/query-helpers';
 import { send, listen } from 'loot-core/src/platform/client/fetch';
 import { currentDay } from 'loot-core/src/shared/months';
-import { q } from 'loot-core/src/shared/query';
+import { q, type Query } from 'loot-core/src/shared/query';
 import { getScheduledAmount } from 'loot-core/src/shared/schedules';
 import {
   updateTransaction,
@@ -28,15 +36,28 @@ import {
   makeAsNonChildTransactions,
 } from 'loot-core/src/shared/transactions';
 import { applyChanges, groupById } from 'loot-core/src/shared/util';
+import {
+  type NewRuleEntity,
+  type RuleActionEntity,
+  type AccountEntity,
+  type PayeeEntity,
+  type RuleConditionEntity,
+  type TransactionEntity,
+  type TransactionFilterEntity,
+} from 'loot-core/src/types/models';
 
 import { useAccounts } from '../../hooks/useAccounts';
 import { useActions } from '../../hooks/useActions';
 import { useCategories } from '../../hooks/useCategories';
 import { useDateFormat } from '../../hooks/useDateFormat';
 import { useFailedAccounts } from '../../hooks/useFailedAccounts';
+import { useLocalPref } from '../../hooks/useLocalPref';
 import { usePayees } from '../../hooks/usePayees';
 import { usePreviewTransactions } from '../../hooks/usePreviewTransactions';
-import { SelectedProviderWithItems } from '../../hooks/useSelected';
+import {
+  SelectedProviderWithItems,
+  type Actions,
+} from '../../hooks/useSelected';
 import {
   SplitsExpandedProvider,
   useSplitsExpanded,
@@ -51,7 +72,19 @@ import { TransactionList } from '../transactions/TransactionList';
 
 import { AccountHeader } from './Header';
 
-function EmptyMessage({ onAdd }) {
+type ConditionEntity = Partial<RuleConditionEntity> | TransactionFilterEntity;
+
+function isTransactionFilterEntity(
+  filter: ConditionEntity,
+): filter is TransactionFilterEntity {
+  return 'id' in filter;
+}
+
+type EmptyMessageProps = {
+  onAdd: () => void;
+};
+
+function EmptyMessage({ onAdd }: EmptyMessageProps) {
   return (
     <View
       style={{
@@ -98,22 +131,34 @@ function EmptyMessage({ onAdd }) {
   );
 }
 
+type AllTransactionsProps = {
+  account?: AccountEntity;
+  transactions: TransactionEntity[];
+  balances: Record<string, { balance: number }> | null;
+  showBalances?: boolean;
+  filtered?: boolean;
+  children: (
+    transactions: TransactionEntity[],
+    balances: Record<string, { balance: number }> | null,
+  ) => ReactElement;
+  collapseTransactions: (ids: string[]) => void;
+};
+
 function AllTransactions({
-  account = {},
+  account,
   transactions,
   balances,
   showBalances,
   filtered,
   children,
   collapseTransactions,
-}) {
-  const accountId = account.id;
-  const prependTransactions = usePreviewTransactions(collapseTransactions).map(
-    trans => ({
+}: AllTransactionsProps) {
+  const accountId = account?.id;
+  const prependTransactions: (TransactionEntity & { _inverse?: boolean })[] =
+    usePreviewTransactions(collapseTransactions).map(trans => ({
       ...trans,
       _inverse: accountId ? accountId !== trans.account : false,
-    }),
-  );
+    }));
 
   transactions ??= [];
 
@@ -140,6 +185,8 @@ function AllTransactions({
           (scheduledTransaction._inverse ? -1 : 1) *
           getScheduledAmount(scheduledTransaction.amount);
         return {
+          // TODO: fix me
+          // eslint-disable-next-line react-hooks/exhaustive-deps
           balance: (runningBalance += amount),
           id: scheduledTransaction.id,
         };
@@ -169,7 +216,11 @@ function AllTransactions({
   return children(allTransactions, allBalances);
 }
 
-function getField(field) {
+function getField(field?: string) {
+  if (!field) {
+    return 'date';
+  }
+
   switch (field) {
     case 'account':
       return 'account.name';
@@ -186,37 +237,120 @@ function getField(field) {
   }
 }
 
-class AccountInternal extends PureComponent {
-  constructor(props) {
+type AccountInternalProps = {
+  accountId?: string;
+  filterConditions: RuleConditionEntity[];
+  showBalances?: boolean;
+  setShowBalances: (newValue: boolean) => void;
+  showCleared?: boolean;
+  setShowCleared: (newValue: boolean) => void;
+  showReconciled: boolean;
+  setShowReconciled: (newValue: boolean) => void;
+  showExtraBalances?: boolean;
+  setShowExtraBalances: (newValue: boolean) => void;
+  modalShowing?: boolean;
+  setLastUndoState: (state: null) => void;
+  lastUndoState: { current: UndoState | null };
+  accounts: AccountEntity[];
+  getPayees: () => Promise<PayeeEntity[]>;
+  updateAccount: (newAccount: AccountEntity) => void;
+  newTransactions: string[];
+  matchedTransactions: string[];
+  splitsExpandedDispatch: ReturnType<typeof useSplitsExpanded>['dispatch'];
+  expandSplits?: boolean;
+  savedFilters: TransactionFilterEntity[];
+  onBatchEdit: ReturnType<typeof useTransactionBatchActions>['onBatchEdit'];
+  onBatchDuplicate: ReturnType<
+    typeof useTransactionBatchActions
+  >['onBatchDuplicate'];
+  onBatchLinkSchedule: ReturnType<
+    typeof useTransactionBatchActions
+  >['onBatchLinkSchedule'];
+  onBatchUnlinkSchedule: ReturnType<
+    typeof useTransactionBatchActions
+  >['onBatchUnlinkSchedule'];
+  onBatchDelete: ReturnType<typeof useTransactionBatchActions>['onBatchDelete'];
+  categoryId?: string;
+  location: ReturnType<typeof useLocation>;
+  failedAccounts: ReturnType<typeof useFailedAccounts>;
+  dateFormat: ReturnType<typeof useDateFormat>;
+  payees: ReturnType<typeof usePayees>;
+  categoryGroups: ReturnType<typeof useCategories>['grouped'];
+  hideFraction: boolean;
+  accountsSyncing: string[];
+} & ReturnType<typeof useActions>;
+type AccountInternalState = {
+  search: string;
+  filterConditions: ConditionEntity[];
+  filterId: Record<string, unknown>;
+  filterConditionsOp: 'and' | 'or';
+  loading: boolean;
+  workingHard: boolean;
+  reconcileAmount: null | number;
+  transactions: TransactionEntity[];
+  transactionCount: number;
+  transactionsFiltered?: boolean;
+  showBalances?: boolean;
+  balances: Record<string, { balance: number }> | null;
+  showCleared?: boolean;
+  prevShowCleared?: boolean;
+  showReconciled: boolean;
+  editingName: boolean;
+  isAdding: boolean;
+  modalShowing?: boolean;
+  sort: {
+    ascDesc: 'asc' | 'desc';
+    field: string;
+    prevField?: string;
+    prevAscDesc?: 'asc' | 'desc';
+  } | null;
+  filteredAmount: null | number;
+};
+
+class AccountInternal extends PureComponent<
+  AccountInternalProps,
+  AccountInternalState
+> {
+  paged: ReturnType<typeof pagedQuery> | null;
+  rootQuery: Query;
+  currentQuery: Query;
+  table: MutableRefObject<{
+    edit: (updatedId: string | null, op?: string, someBool?: boolean) => void;
+    setRowAnimation: (animation: boolean) => void;
+    scrollTo: (focusId: string) => void;
+    scrollToTop: () => void;
+  } | null>;
+  unlisten?: () => void;
+  dispatchSelected?: (action: Actions) => void;
+
+  constructor(props: AccountInternalProps) {
     super(props);
     this.paged = null;
     this.table = createRef();
-    this.animated = true;
 
     this.state = {
       search: '',
       filterConditions: props.filterConditions || [],
-      filterId: [],
+      filterId: {},
       filterConditionsOp: 'and',
       loading: true,
       workingHard: false,
       reconcileAmount: null,
       transactions: [],
-      transactionsCount: 0,
+      transactionCount: 0,
       showBalances: props.showBalances,
       balances: null,
       showCleared: props.showCleared,
       showReconciled: props.showReconciled,
       editingName: false,
       isAdding: false,
-      latestDate: null,
-      sort: [],
+      sort: null,
       filteredAmount: null,
     };
   }
 
   async componentDidMount() {
-    const maybeRefetch = tables => {
+    const maybeRefetch = (tables: string[]) => {
       if (
         tables.includes('transactions') ||
         tables.includes('category_mapping') ||
@@ -226,14 +360,14 @@ class AccountInternal extends PureComponent {
       }
     };
 
-    const onUndo = async ({ tables, messages }) => {
+    const onUndo = async ({ tables, messages }: UndoState) => {
       await maybeRefetch(tables);
 
       // If all the messages are dealing with transactions, find the
       // first message referencing a non-deleted row so that we can
       // highlight the row
       //
-      let focusId;
+      let focusId: null | string;
       if (
         messages.every(msg => msg.dataset === 'transactions') &&
         !messages.find(msg => msg.column === 'tombstone')
@@ -282,7 +416,7 @@ class AccountInternal extends PureComponent {
     }
   }
 
-  componentDidUpdate(prevProps) {
+  componentDidUpdate(prevProps: AccountInternalProps) {
     // If the active account changes - close the transaction entry mode
     if (this.state.isAdding && this.props.accountId !== prevProps.accountId) {
       this.setState({ isAdding: false });
@@ -302,7 +436,7 @@ class AccountInternal extends PureComponent {
 
     //Resest sort/filter/search on account change
     if (this.props.accountId !== prevProps.accountId) {
-      this.setState({ sort: [], search: '', filterConditions: [] });
+      this.setState({ sort: null, search: '', filterConditions: [] });
     }
   }
 
@@ -316,12 +450,12 @@ class AccountInternal extends PureComponent {
   }
 
   fetchAllIds = async () => {
-    const { data } = await runQuery(this.paged.getQuery().select('id'));
+    const { data } = await runQuery(this.paged?.getQuery().select('id'));
     // Remember, this is the `grouped` split type so we need to deal
     // with the `subtransactions` property
-    return data.reduce((arr, t) => {
+    return data.reduce((arr: string[], t: TransactionEntity) => {
       arr.push(t.id);
-      t.subtransactions.forEach(sub => arr.push(sub.id));
+      t.subtransactions?.forEach(sub => arr.push(sub.id));
       return arr;
     }, []);
   };
@@ -330,7 +464,7 @@ class AccountInternal extends PureComponent {
     this.paged?.run();
   };
 
-  fetchTransactions = filterConditions => {
+  fetchTransactions = (filterConditions?: ConditionEntity[]) => {
     const query = this.makeRootQuery();
     this.rootQuery = this.currentQuery = query;
     if (filterConditions) this.applyFilters(filterConditions);
@@ -347,7 +481,7 @@ class AccountInternal extends PureComponent {
     return queries.makeTransactionsQuery(accountId);
   };
 
-  updateQuery(query, isFiltered) {
+  updateQuery(query: Query, isFiltered: boolean = false) {
     if (this.paged) {
       this.paged.unsubscribe();
     }
@@ -359,7 +493,10 @@ class AccountInternal extends PureComponent {
 
     this.paged = pagedQuery(
       query.select('*'),
-      async (data, prevData) => {
+      async (
+        data: TransactionEntity[],
+        prevData: TransactionEntity[] | null,
+      ) => {
         const firstLoad = prevData == null;
 
         if (firstLoad) {
@@ -381,7 +518,7 @@ class AccountInternal extends PureComponent {
         this.setState(
           {
             transactions: data,
-            transactionCount: this.paged.getTotalCount(),
+            transactionCount: this.paged?.getTotalCount(),
             transactionsFiltered: isFiltered,
             loading: false,
             workingHard: false,
@@ -409,7 +546,7 @@ class AccountInternal extends PureComponent {
     );
   }
 
-  UNSAFE_componentWillReceiveProps(nextProps) {
+  UNSAFE_componentWillReceiveProps(nextProps: AccountInternalProps) {
     if (this.props.accountId !== nextProps.accountId) {
       this.setState(
         {
@@ -429,8 +566,8 @@ class AccountInternal extends PureComponent {
     }
   }
 
-  onSearch = value => {
-    this.paged.unsubscribe();
+  onSearch = (value: string) => {
+    this.paged?.unsubscribe();
     this.setState({ search: value }, this.onSearchDone);
   };
 
@@ -481,7 +618,7 @@ class AccountInternal extends PureComponent {
           accountId,
           categories,
           filename: res[0],
-          onImported: didChange => {
+          onImported: (didChange: boolean) => {
             if (didChange) {
               this.fetchTransactions();
             }
@@ -491,7 +628,7 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onExport = async accountName => {
+  onExport = async (accountName: string) => {
     const { t } = useTranslation();
 
     const exportedTransactions = await send('transactions-export-query', {
@@ -508,10 +645,13 @@ class AccountInternal extends PureComponent {
     );
   };
 
-  onTransactionsChange = (newTransaction, data) => {
+  onTransactionsChange = (
+    newTransaction: TransactionEntity,
+    data: TransactionEntity[],
+  ) => {
     // Apply changes to pagedQuery data
-    this.paged.optimisticUpdate(
-      data => {
+    this.paged?.optimisticUpdate(
+      (data: TransactionEntity[]) => {
         if (newTransaction._deleted) {
           return data.filter(t => t.id !== newTransaction.id);
         } else {
@@ -537,7 +677,7 @@ class AccountInternal extends PureComponent {
       account &&
       this.state.search === '' &&
       this.state.filterConditions.length === 0 &&
-      (this.state.sort.length === 0 ||
+      (this.state.sort === null ||
         (this.state.sort.field === 'date' &&
           this.state.sort.ascDesc === 'desc'))
     );
@@ -550,45 +690,53 @@ class AccountInternal extends PureComponent {
 
     const { data } = await runQuery(
       this.paged
-        .getQuery()
+        ?.getQuery()
         .options({ splits: 'none' })
         .select([{ balance: { $sumOver: '$amount' } }]),
     );
 
-    return groupById(data);
+    return groupById<{ id: string; balance: number }>(data);
   }
 
   onAddTransaction = () => {
     this.setState({ isAdding: true });
   };
 
-  onExposeName = flag => {
+  onExposeName = (flag: boolean) => {
     this.setState({ editingName: flag });
   };
 
-  onSaveName = name => {
+  onSaveName = (name: string) => {
     if (name.trim().length) {
       const accountId = this.props.accountId;
       const account = this.props.accounts.find(
         account => account.id === accountId,
-      );
+      )!;
       this.props.updateAccount({ ...account, name });
       this.setState({ editingName: false });
     }
   };
 
   onToggleExtraBalances = () => {
-    const { accountId, showExtraBalances } = this.props;
-    const key = 'show-extra-balances-' + accountId || 'all-accounts';
-
-    this.props.savePrefs({ [key]: !showExtraBalances });
+    this.props.setShowExtraBalances(!this.props.showExtraBalances);
   };
 
-  onMenuSelect = async item => {
-    const accountId = this.props.accountId;
+  onMenuSelect = async (
+    item:
+      | 'link'
+      | 'unlink'
+      | 'close'
+      | 'reopen'
+      | 'export'
+      | 'toggle-balance'
+      | 'remove-sorting'
+      | 'toggle-cleared'
+      | 'toggle-reconciled',
+  ) => {
+    const accountId = this.props.accountId!;
     const account = this.props.accounts.find(
       account => account.id === accountId,
-    );
+    )!;
 
     switch (item) {
       case 'link':
@@ -616,17 +764,17 @@ class AccountInternal extends PureComponent {
         break;
       case 'toggle-balance':
         if (this.state.showBalances) {
-          this.props.savePrefs({ ['show-balances-' + accountId]: false });
+          this.props.setShowBalances(false);
           this.setState({ showBalances: false, balances: null });
         } else {
-          this.props.savePrefs({ ['show-balances-' + accountId]: true });
+          this.props.setShowBalances(true);
           this.setState(
             {
               transactions: [],
               transactionCount: 0,
               filterConditions: [],
               search: '',
-              sort: [],
+              sort: null,
               showBalances: true,
             },
             () => {
@@ -636,7 +784,7 @@ class AccountInternal extends PureComponent {
         }
         break;
       case 'remove-sorting': {
-        this.setState({ sort: [] }, () => {
+        this.setState({ sort: null }, () => {
           const filterConditions = this.state.filterConditions;
           if (filterConditions.length > 0) {
             this.applyFilters([...filterConditions]);
@@ -651,21 +799,21 @@ class AccountInternal extends PureComponent {
       }
       case 'toggle-cleared':
         if (this.state.showCleared) {
-          this.props.savePrefs({ ['hide-cleared-' + accountId]: true });
+          this.props.setShowCleared(false);
           this.setState({ showCleared: false });
         } else {
-          this.props.savePrefs({ ['hide-cleared-' + accountId]: false });
+          this.props.setShowCleared(true);
           this.setState({ showCleared: true });
         }
         break;
       case 'toggle-reconciled':
         if (this.state.showReconciled) {
-          this.props.savePrefs({ ['hide-reconciled-' + accountId]: true });
+          this.props.setShowReconciled(false);
           this.setState({ showReconciled: false }, () =>
             this.fetchTransactions(this.state.filterConditions),
           );
         } else {
-          this.props.savePrefs({ ['hide-reconciled-' + accountId]: false });
+          this.props.setShowReconciled(true);
           this.setState({ showReconciled: true }, () =>
             this.fetchTransactions(this.state.filterConditions),
           );
@@ -675,7 +823,7 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  getAccountTitle(account, id) {
+  getAccountTitle(account?: AccountEntity, id?: string) {
     const { filterName } = this.props.location.state || {};
 
     if (filterName) {
@@ -698,7 +846,7 @@ class AccountInternal extends PureComponent {
     return account.name;
   }
 
-  getBalanceQuery(account, id) {
+  getBalanceQuery(id?: string) {
     return {
       name: `balance-query-${id}`,
       query: this.makeRootQuery().calculate({ $sum: '$amount' }),
@@ -707,20 +855,20 @@ class AccountInternal extends PureComponent {
 
   getFilteredAmount = async () => {
     const { data: amount } = await runQuery(
-      this.paged.getQuery().calculate({ $sum: '$amount' }),
+      this.paged?.getQuery().calculate({ $sum: '$amount' }),
     );
     return amount;
   };
 
-  isNew = id => {
+  isNew = (id: string) => {
     return this.props.newTransactions.includes(id);
   };
 
-  isMatched = id => {
+  isMatched = (id: string) => {
     return this.props.matchedTransactions.includes(id);
   };
 
-  onCreatePayee = name => {
+  onCreatePayee = (name: string) => {
     const trimmed = name.trim();
     if (trimmed !== '') {
       return this.props.createPayee(name);
@@ -741,7 +889,9 @@ class AccountInternal extends PureComponent {
     );
     let transactions = ungroupTransactions(data);
 
-    const changes = { updated: [] };
+    const changes: { updated: Array<Partial<TransactionEntity>> } = {
+      updated: [],
+    };
 
     transactions.forEach(trans => {
       const { diff } = updateTransaction(transactions, {
@@ -760,7 +910,7 @@ class AccountInternal extends PureComponent {
     await this.refetchTransactions();
   };
 
-  onReconcile = async balance => {
+  onReconcile = async (balance: number) => {
     this.setState(({ showCleared }) => ({
       reconcileAmount: balance,
       showCleared: true,
@@ -788,7 +938,7 @@ class AccountInternal extends PureComponent {
       }
     });
 
-    const targetDiff = reconcileAmount - cleared;
+    const targetDiff = (reconcileAmount || 0) - cleared;
 
     if (targetDiff === 0) {
       await this.lockTransactions();
@@ -800,14 +950,14 @@ class AccountInternal extends PureComponent {
     });
   };
 
-  onCreateReconciliationTransaction = async diff => {
+  onCreateReconciliationTransaction = async (diff: number) => {
     const { t } = useTranslation();
 
     // Create a new reconciliation transaction
     const reconciliationTransactions = realizeTempTransactions([
       {
         id: 'temp',
-        account: this.props.accountId,
+        account: this.props.accountId!,
         cleared: true,
         reconciled: false,
         amount: diff,
@@ -828,7 +978,7 @@ class AccountInternal extends PureComponent {
     await this.refetchTransactions();
   };
 
-  onShowTransactions = async ids => {
+  onShowTransactions = async (ids: string[]) => {
     const { t } = useTranslation();
 
     this.onApplyFilter({
@@ -837,7 +987,7 @@ class AccountInternal extends PureComponent {
     });
   };
 
-  onBatchEdit = (name, ids) => {
+  onBatchEdit = (name: keyof TransactionEntity, ids: string[]) => {
     this.props.onBatchEdit({
       name,
       ids,
@@ -851,24 +1001,26 @@ class AccountInternal extends PureComponent {
     });
   };
 
-  onBatchDuplicate = ids => {
+  onBatchDuplicate = (ids: string[]) => {
     this.props.onBatchDuplicate({ ids, onSuccess: this.refetchTransactions });
   };
 
-  onBatchDelete = ids => {
+  onBatchDelete = (ids: string[]) => {
     this.props.onBatchDelete({ ids, onSuccess: this.refetchTransactions });
   };
 
-  onMakeAsSplitTransaction = async ids => {
+  onMakeAsSplitTransaction = async (ids: string[]) => {
     this.setState({ workingHard: true });
 
-    const { data: transactions } = await runQuery(
+    const { data } = await runQuery(
       q('transactions')
         .filter({ id: { $oneof: ids } })
         .select('*')
         .options({ splits: 'none' }),
     );
 
+    const transactions: TransactionEntity[] = data;
+
     if (!transactions || transactions.length === 0) {
       return;
     }
@@ -896,17 +1048,22 @@ class AccountInternal extends PureComponent {
     this.refetchTransactions();
   };
 
-  onMakeAsNonSplitTransactions = async ids => {
+  onMakeAsNonSplitTransactions = async (ids: string[]) => {
     this.setState({ workingHard: true });
 
-    const { data: groupedTransactions } = await runQuery(
+    const { data } = await runQuery(
       q('transactions')
         .filter({ id: { $oneof: ids } })
         .select('*')
         .options({ splits: 'grouped' }),
     );
 
-    let changes = {
+    const groupedTransactions: TransactionEntity[] = data;
+
+    let changes: {
+      updated: TransactionEntity[];
+      deleted: TransactionEntity[];
+    } = {
       updated: [],
       deleted: [],
     };
@@ -961,13 +1118,17 @@ class AccountInternal extends PureComponent {
     this.refetchTransactions();
 
     const transactionsToSelect = changes.updated.map(t => t.id);
-    this.dispatchSelected({
+    this.dispatchSelected?.({
       type: 'select-all',
       ids: transactionsToSelect,
     });
   };
 
-  checkForReconciledTransactions = async (ids, confirmReason, onConfirm) => {
+  checkForReconciledTransactions = async (
+    ids: string[],
+    confirmReason: string,
+    onConfirm: (ids: string[]) => void,
+  ) => {
     const { data } = await runQuery(
       q('transactions')
         .filter({ id: { $oneof: ids }, reconciled: true })
@@ -987,7 +1148,7 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onBatchLinkSchedule = ids => {
+  onBatchLinkSchedule = (ids: string[]) => {
     this.props.onBatchLinkSchedule({
       ids,
       account: this.props.accounts.find(a => a.id === this.props.accountId),
@@ -995,14 +1156,14 @@ class AccountInternal extends PureComponent {
     });
   };
 
-  onBatchUnlinkSchedule = ids => {
+  onBatchUnlinkSchedule = (ids: string[]) => {
     this.props.onBatchUnlinkSchedule({
       ids,
       onSuccess: this.refetchTransactions,
     });
   };
 
-  onCreateRule = async ids => {
+  onCreateRule = async (ids: string[]) => {
     const { data } = await runQuery(
       q('transactions')
         .filter({ id: { $oneof: ids } })
@@ -1017,27 +1178,27 @@ class AccountInternal extends PureComponent {
     );
 
     const payeeCondition = ruleTransaction.imported_payee
-      ? {
+      ? ({
           field: 'imported_payee',
           op: 'is',
           value: ruleTransaction.imported_payee,
           type: 'string',
-        }
-      : {
+        } satisfies RuleConditionEntity)
+      : ({
           field: 'payee',
           op: 'is',
-          value: ruleTransaction.payee,
+          value: ruleTransaction.payee!,
           type: 'id',
-        };
+        } satisfies RuleConditionEntity);
     const amountCondition = {
       field: 'amount',
       op: 'isapprox',
       value: ruleTransaction.amount,
       type: 'number',
-    };
+    } satisfies RuleConditionEntity;
 
     const rule = {
-      stage: null,
+      stage: 'default',
       conditionsOp: 'and',
       conditions: [payeeCondition, amountCondition],
       actions: [
@@ -1051,7 +1212,7 @@ class AccountInternal extends PureComponent {
                 options: {
                   splitIndex: 0,
                 },
-              },
+              } satisfies RuleActionEntity,
             ]
           : []),
         ...childTransactions.flatMap((sub, index) => [
@@ -1062,7 +1223,7 @@ class AccountInternal extends PureComponent {
               splitIndex: index + 1,
               method: 'fixed-amount',
             },
-          },
+          } satisfies RuleActionEntity,
           {
             op: 'set',
             field: 'category',
@@ -1071,16 +1232,16 @@ class AccountInternal extends PureComponent {
             options: {
               splitIndex: index + 1,
             },
-          },
+          } satisfies RuleActionEntity,
         ]),
       ],
-    };
+    } satisfies NewRuleEntity;
 
     this.props.pushModal('edit-rule', { rule });
   };
 
-  onSetTransfer = async ids => {
-    const onConfirmTransfer = async ids => {
+  onSetTransfer = async (ids: string[]) => {
+    const onConfirmTransfer = async (ids: string[]) => {
       this.setState({ workingHard: true });
 
       const payees = await this.props.getPayees();
@@ -1101,12 +1262,12 @@ class AccountInternal extends PureComponent {
           updated: [
             {
               ...fromTrans,
-              payee: toPayee.id,
+              payee: toPayee?.id,
               transfer_id: toTrans.id,
             },
             {
               ...toTrans,
-              payee: fromPayee.id,
+              payee: fromPayee?.id,
               transfer_id: fromTrans.id,
             },
           ],
@@ -1125,7 +1286,7 @@ class AccountInternal extends PureComponent {
     );
   };
 
-  onConditionsOpChange = value => {
+  onConditionsOpChange = (value: 'and' | 'or') => {
     this.setState({ filterConditionsOp: value });
     this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
     this.applyFilters([...this.state.filterConditions]);
@@ -1134,16 +1295,21 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onReloadSavedFilter = (savedFilter, item) => {
+  onReloadSavedFilter = (
+    savedFilter: TransactionFilterEntity & { status?: string },
+    item: string,
+  ) => {
     if (item === 'reload') {
       const [savedFilter] = this.props.savedFilters.filter(
         f => f.id === this.state.filterId.id,
       );
-      this.setState({ filterConditionsOp: savedFilter.conditionsOp });
+      this.setState({ filterConditionsOp: savedFilter.conditionsOp ?? 'and' });
       this.applyFilters([...savedFilter.conditions]);
     } else {
       if (savedFilter.status) {
-        this.setState({ filterConditionsOp: savedFilter.conditionsOp });
+        this.setState({
+          filterConditionsOp: savedFilter.conditionsOp ?? 'and',
+        });
         this.applyFilters([...savedFilter.conditions]);
       }
     }
@@ -1152,14 +1318,17 @@ class AccountInternal extends PureComponent {
 
   onClearFilters = () => {
     this.setState({ filterConditionsOp: 'and' });
-    this.setState({ filterId: [] });
+    this.setState({ filterId: {} });
     this.applyFilters([]);
     if (this.state.search !== '') {
       this.onSearch(this.state.search);
     }
   };
 
-  onUpdateFilter = (oldCondition, updatedCondition) => {
+  onUpdateFilter = (
+    oldCondition: RuleConditionEntity,
+    updatedCondition: RuleConditionEntity,
+  ) => {
     this.applyFilters(
       this.state.filterConditions.map(c =>
         c === oldCondition ? updatedCondition : c,
@@ -1176,10 +1345,10 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onDeleteFilter = condition => {
+  onDeleteFilter = (condition: RuleConditionEntity) => {
     this.applyFilters(this.state.filterConditions.filter(c => c !== condition));
     if (this.state.filterConditions.length === 1) {
-      this.setState({ filterId: [] });
+      this.setState({ filterId: {} });
       this.setState({ filterConditionsOp: 'and' });
     } else {
       this.setState({
@@ -1194,14 +1363,9 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  onApplyFilter = async conditionOrSavedFilter => {
+  onApplyFilter = async (conditionOrSavedFilter: ConditionEntity) => {
     let filterConditions = this.state.filterConditions;
-    if (conditionOrSavedFilter.customName) {
-      filterConditions = filterConditions.filter(
-        c => c.customName !== conditionOrSavedFilter.customName,
-      );
-    }
-    if (conditionOrSavedFilter.conditions) {
+    if (isTransactionFilterEntity(conditionOrSavedFilter)) {
       // A saved filter was passed in.
       const savedFilter = conditionOrSavedFilter;
       this.setState({
@@ -1210,6 +1374,12 @@ class AccountInternal extends PureComponent {
       this.setState({ filterConditionsOp: savedFilter.conditionsOp });
       this.applyFilters([...savedFilter.conditions]);
     } else {
+      filterConditions = filterConditions.filter(
+        c =>
+          !isTransactionFilterEntity(c) &&
+          c.customName !== conditionOrSavedFilter.customName,
+      );
+
       // A condition was passed in.
       const condition = conditionOrSavedFilter;
       this.setState({
@@ -1220,12 +1390,16 @@ class AccountInternal extends PureComponent {
       });
       this.applyFilters([...filterConditions, condition]);
     }
+
     if (this.state.search !== '') {
       this.onSearch(this.state.search);
     }
   };
 
-  onScheduleAction = async (name, ids) => {
+  onScheduleAction = async (
+    name: 'skip' | 'post-transaction',
+    ids: string[],
+  ) => {
     switch (name) {
       case 'post-transaction':
         for (const id of ids) {
@@ -1244,15 +1418,19 @@ class AccountInternal extends PureComponent {
     }
   };
 
-  applyFilters = async conditions => {
+  applyFilters = async (conditions: ConditionEntity[]) => {
     if (conditions.length > 0) {
-      const customQueryFilters = conditions
-        .filter(cond => !!cond.customName)
-        .map(f => f.queryFilter);
+      const filteredCustomQueryFilters: Partial<RuleConditionEntity>[] =
+        conditions.filter(cond => !isTransactionFilterEntity(cond));
+      const customQueryFilters = filteredCustomQueryFilters.map(
+        f => f.queryFilter,
+      );
       const { filters: queryFilters } = await send(
         'make-filters-from-conditions',
         {
-          conditions: conditions.filter(cond => !cond.customName),
+          conditions: conditions.filter(
+            cond => isTransactionFilterEntity(cond) || !cond.customName,
+          ),
         },
       );
       const conditionsOpKey =
@@ -1282,24 +1460,33 @@ class AccountInternal extends PureComponent {
       );
     }
 
-    if (this.state.sort.length !== 0) {
+    if (this.state.sort !== null) {
       this.applySort();
     }
   };
 
-  applySort = (field, ascDesc, prevField, prevAscDesc) => {
+  applySort = (
+    field?: string,
+    ascDesc?: 'asc' | 'desc',
+    prevField?: string,
+    prevAscDesc?: 'asc' | 'desc',
+  ) => {
     const filterConditions = this.state.filterConditions;
     const isFiltered = filterConditions.length > 0;
-    const sortField = getField(!field ? this.state.sort.field : field);
-    const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
+    const sortField = getField(!field ? this.state.sort?.field : field);
+    const sortAscDesc = !ascDesc ? this.state.sort?.ascDesc : ascDesc;
     const sortPrevField = getField(
-      !prevField ? this.state.sort.prevField : prevField,
+      !prevField ? this.state.sort?.prevField : prevField,
     );
     const sortPrevAscDesc = !prevField
-      ? this.state.sort.prevAscDesc
+      ? this.state.sort?.prevAscDesc
       : prevAscDesc;
 
-    const sortCurrentQuery = function (that, sortField, sortAscDesc) {
+    const sortCurrentQuery = function (
+      that: AccountInternal,
+      sortField: string,
+      sortAscDesc?: 'asc' | 'desc',
+    ) {
       if (sortField === 'cleared') {
         that.currentQuery = that.currentQuery.orderBy({
           reconciled: sortAscDesc,
@@ -1311,7 +1498,11 @@ class AccountInternal extends PureComponent {
       });
     };
 
-    const sortRootQuery = function (that, sortField, sortAscDesc) {
+    const sortRootQuery = function (
+      that: AccountInternal,
+      sortField: string,
+      sortAscDesc?: 'asc' | 'desc',
+    ) {
       if (sortField === 'cleared') {
         that.currentQuery = that.rootQuery.orderBy({
           reconciled: sortAscDesc,
@@ -1328,9 +1519,9 @@ class AccountInternal extends PureComponent {
 
     // sort by previously used sort field, if any
     const maybeSortByPreviousField = function (
-      that,
-      sortPrevField,
-      sortPrevAscDesc,
+      that: AccountInternal,
+      sortPrevField: string,
+      sortPrevAscDesc?: 'asc' | 'desc',
     ) {
       if (!sortPrevField) {
         return;
@@ -1373,12 +1564,12 @@ class AccountInternal extends PureComponent {
     this.updateQuery(this.currentQuery, isFiltered);
   };
 
-  onSort = (headerClicked, ascDesc) => {
-    let prevField;
-    let prevAscDesc;
+  onSort = (headerClicked: string, ascDesc: 'asc' | 'desc') => {
+    let prevField: string | undefined;
+    let prevAscDesc: 'asc' | 'desc' | undefined;
     //if staying on same column but switching asc/desc
     //then keep prev the same
-    if (headerClicked === this.state.sort.field) {
+    if (headerClicked === this.state.sort?.field) {
       prevField = this.state.sort.prevField;
       prevAscDesc = this.state.sort.prevAscDesc;
       this.setState({
@@ -1390,14 +1581,14 @@ class AccountInternal extends PureComponent {
     } else {
       //if switching to new column then capture state
       //of current sort column as prev
-      prevField = this.state.sort.field;
-      prevAscDesc = this.state.sort.ascDesc;
+      prevField = this.state.sort?.field;
+      prevAscDesc = this.state.sort?.ascDesc;
       this.setState({
         sort: {
           field: headerClicked,
           ascDesc,
-          prevField: this.state.sort.field,
-          prevAscDesc: this.state.sort.ascDesc,
+          prevField: this.state.sort?.field,
+          prevAscDesc: this.state.sort?.ascDesc,
         },
       });
     }
@@ -1449,7 +1640,7 @@ class AccountInternal extends PureComponent {
 
     const category = categoryGroups
       .flatMap(g => g.categories)
-      .find(category => category.id === categoryId);
+      .find(category => category?.id === categoryId);
 
     const showEmptyMessage = !loading && !accountId && accounts.length === 0;
 
@@ -1459,7 +1650,7 @@ class AccountInternal extends PureComponent {
       accountId !== 'offbudget' &&
       accountId !== 'uncategorized';
 
-    const balanceQuery = this.getBalanceQuery(account, accountId);
+    const balanceQuery = this.getBalanceQuery(accountId);
 
     return (
       <AllTransactions
@@ -1489,7 +1680,6 @@ class AccountInternal extends PureComponent {
                 account={account}
                 filterId={filterId}
                 savedFilters={this.props.savedFilters}
-                location={this.props.location}
                 accountName={accountName}
                 accountsSyncing={accountsSyncing}
                 failedAccounts={failedAccounts}
@@ -1504,12 +1694,11 @@ class AccountInternal extends PureComponent {
                 canCalculateBalance={this.canCalculateBalance}
                 filteredAmount={filteredAmount}
                 isFiltered={transactionsFiltered}
-                isSorted={this.state.sort.length !== 0}
+                isSorted={this.state.sort !== null}
                 reconcileAmount={reconcileAmount}
                 search={this.state.search}
                 filterConditions={this.state.filterConditions}
                 filterConditionsOp={this.state.filterConditionsOp}
-                savePrefs={this.props.savePrefs}
                 pushModal={this.props.pushModal}
                 onSearch={this.onSearch}
                 onShowTransactions={this.onShowTransactions}
@@ -1545,11 +1734,11 @@ class AccountInternal extends PureComponent {
 
               <View style={{ flex: 1 }}>
                 <TransactionList
+                  headerContent={undefined}
                   tableRef={this.table}
                   account={account}
                   transactions={transactions}
                   allTransactions={allTransactions}
-                  animated={this.animated}
                   loadMoreTransactions={() =>
                     this.paged && this.paged.fetchNext()
                   }
@@ -1590,16 +1779,10 @@ class AccountInternal extends PureComponent {
                     ) : null
                   }
                   onSort={this.onSort}
-                  sortField={this.state.sort.field}
-                  ascDesc={this.state.sort.ascDesc}
+                  sortField={this.state.sort?.field}
+                  ascDesc={this.state.sort?.ascDesc}
                   onChange={this.onTransactionsChange}
                   onRefetch={this.refetchTransactions}
-                  onRefetchUpToRow={row =>
-                    this.paged.refetchUpToRow(row, {
-                      field: 'date',
-                      order: 'desc',
-                    })
-                  }
                   onCloseAddTransaction={() =>
                     this.setState({ isAdding: false })
                   }
@@ -1615,7 +1798,17 @@ class AccountInternal extends PureComponent {
   }
 }
 
-function AccountHack(props) {
+type AccountHackProps = Omit<
+  AccountInternalProps,
+  | 'splitsExpandedDispatch'
+  | 'onBatchEdit'
+  | 'onBatchDuplicate'
+  | 'onBatchLinkSchedule'
+  | 'onBatchUnlinkSchedule'
+  | 'onBatchDelete'
+>;
+
+function AccountHack(props: AccountHackProps) {
   const { dispatch: splitsExpandedDispatch } = useSplitsExpanded();
   const {
     onBatchEdit,
@@ -1652,11 +1845,17 @@ export function Account() {
   const failedAccounts = useFailedAccounts();
   const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const [hideFraction = false] = useSyncedPref('hideFraction');
-  const [expandSplits] = useSyncedPref('expand-splits');
-  const [showBalances] = useSyncedPref(`show-balances-${params.id}`);
-  const [hideCleared] = useSyncedPref(`hide-cleared-${params.id}`);
-  const [hideReconciled] = useSyncedPref(`hide-reconciled-${params.id}`);
-  const [showExtraBalances] = useSyncedPref(
+  const [expandSplits] = useLocalPref('expand-splits');
+  const [showBalances, setShowBalances] = useSyncedPref(
+    `show-balances-${params.id}`,
+  );
+  const [hideCleared, setHideCleared] = useSyncedPref(
+    `hide-cleared-${params.id}`,
+  );
+  const [hideReconciled, setHideReconciled] = useSyncedPref(
+    `hide-reconciled-${params.id}`,
+  );
+  const [showExtraBalances, setShowExtraBalances] = useSyncedPref(
     `show-extra-balances-${params.id || 'all-accounts'}`,
   );
   const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
@@ -1683,9 +1882,13 @@ export function Account() {
           hideFraction={hideFraction}
           expandSplits={expandSplits}
           showBalances={showBalances}
+          setShowBalances={setShowBalances}
           showCleared={!hideCleared}
+          setShowCleared={val => setHideCleared(!val)}
           showReconciled={!hideReconciled}
+          setShowReconciled={val => setHideReconciled(!val)}
           showExtraBalances={showExtraBalances}
+          setShowExtraBalances={setShowExtraBalances}
           payees={payees}
           modalShowing={modalShowing}
           accountsSyncing={accountsSyncing}
diff --git a/packages/desktop-client/src/components/filters/FiltersStack.tsx b/packages/desktop-client/src/components/filters/FiltersStack.tsx
index 02576c5de..ec639437e 100644
--- a/packages/desktop-client/src/components/filters/FiltersStack.tsx
+++ b/packages/desktop-client/src/components/filters/FiltersStack.tsx
@@ -24,7 +24,7 @@ export function FiltersStack({
   onConditionsOpChange,
 }: {
   conditions: RuleConditionEntity[];
-  conditionsOp: string;
+  conditionsOp: 'and' | 'or';
   onUpdateFilter: (
     filter: RuleConditionEntity,
     newFilter: RuleConditionEntity,
diff --git a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
index 1a2d68e52..3d4b3d701 100644
--- a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
+++ b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx
@@ -16,7 +16,7 @@ import { NameFilter } from './NameFilter';
 
 export type SavedFilter = {
   conditions?: RuleConditionEntity[];
-  conditionsOp?: string;
+  conditionsOp?: 'and' | 'or';
   id?: string;
   name: string;
   status?: string;
@@ -31,7 +31,7 @@ export function SavedFilterMenuButton({
   savedFilters,
 }: {
   conditions: RuleConditionEntity[];
-  conditionsOp: string;
+  conditionsOp: 'and' | 'or';
   filterId: SavedFilter;
   onClearFilters: () => void;
   onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
diff --git a/packages/desktop-client/src/hooks/useSelected.tsx b/packages/desktop-client/src/hooks/useSelected.tsx
index 15c944e60..a2052e333 100644
--- a/packages/desktop-client/src/hooks/useSelected.tsx
+++ b/packages/desktop-client/src/hooks/useSelected.tsx
@@ -48,7 +48,7 @@ type SelectAllAction = {
   isRangeSelect?: boolean;
 };
 
-type Actions = SelectAction | SelectNoneAction | SelectAllAction;
+export type Actions = SelectAction | SelectNoneAction | SelectAllAction;
 
 export function useSelected<T extends Item>(
   name: string,
@@ -310,7 +310,7 @@ export function SelectedProvider<T extends Item>({
 type SelectedProviderWithItemsProps<T extends Item> = {
   name: string;
   items: T[];
-  initialSelectedIds: string[];
+  initialSelectedIds?: string[];
   fetchAllIds: () => Promise<string[]>;
   registerDispatch?: (dispatch: Dispatch<Actions>) => void;
   selectAllFilter?: (item: T) => boolean;
@@ -322,7 +322,7 @@ type SelectedProviderWithItemsProps<T extends Item> = {
 export function SelectedProviderWithItems<T extends Item>({
   name,
   items,
-  initialSelectedIds,
+  initialSelectedIds = [],
   fetchAllIds,
   registerDispatch,
   selectAllFilter,
diff --git a/packages/loot-core/src/client/data-hooks/filters.ts b/packages/loot-core/src/client/data-hooks/filters.ts
index e26d6fa7e..5b77204a6 100644
--- a/packages/loot-core/src/client/data-hooks/filters.ts
+++ b/packages/loot-core/src/client/data-hooks/filters.ts
@@ -14,7 +14,7 @@ function toJS(rows) {
       tombstone: row.tombstone,
       conditionsOp: row.conditions_op,
       conditions: row.conditions,
-    };
+    } satisfies TransactionFilterEntity;
   });
   return filters;
 }
diff --git a/packages/loot-core/src/client/queries.ts b/packages/loot-core/src/client/queries.ts
index ec5e12497..807ee0eca 100644
--- a/packages/loot-core/src/client/queries.ts
+++ b/packages/loot-core/src/client/queries.ts
@@ -28,7 +28,7 @@ const accountParametrizedField = parametrizedField<'account'>();
 const rolloverParametrizedField = parametrizedField<'rollover-budget'>();
 const reportParametrizedField = parametrizedField<'report-budget'>();
 
-export function getAccountFilter(accountId: string, field = 'account') {
+export function getAccountFilter(accountId?: string, field = 'account') {
   if (accountId) {
     if (accountId === 'budgeted') {
       return {
@@ -64,7 +64,7 @@ export function getAccountFilter(accountId: string, field = 'account') {
   return null;
 }
 
-export function makeTransactionsQuery(accountId: string) {
+export function makeTransactionsQuery(accountId?: string) {
   let query = q('transactions').options({ splits: 'grouped' });
 
   const filter = getAccountFilter(accountId);
diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts
index 078996b3c..785ba31cc 100644
--- a/packages/loot-core/src/client/state-types/modals.d.ts
+++ b/packages/loot-core/src/client/state-types/modals.d.ts
@@ -54,7 +54,7 @@ type FinanceModals = {
   'manage-rules': { payeeId?: string };
   'edit-rule': {
     rule: RuleEntity | NewRuleEntity;
-    onSave: (rule: RuleEntity) => void;
+    onSave?: (rule: RuleEntity) => void;
   };
   'merge-unused-payees': {
     payeeIds: string[];
@@ -270,6 +270,10 @@ type FinanceModals = {
     message?: string;
     onConfirm: () => void;
   };
+  'confirm-unlink-account': {
+    accountName: string;
+    onUnlink: () => void;
+  };
 };
 
 export type PushModalAction = {
diff --git a/packages/loot-core/src/client/state-types/queries.d.ts b/packages/loot-core/src/client/state-types/queries.d.ts
index 2b5138f41..a4a32432c 100644
--- a/packages/loot-core/src/client/state-types/queries.d.ts
+++ b/packages/loot-core/src/client/state-types/queries.d.ts
@@ -3,8 +3,8 @@ import { type AccountEntity } from '../../types/models';
 import type * as constants from '../constants';
 
 export type QueriesState = {
-  newTransactions: unknown[];
-  matchedTransactions: unknown[];
+  newTransactions: string[];
+  matchedTransactions: string[];
   lastTransaction: unknown | null;
   updatedAccounts: string[];
   accounts: AccountEntity[];
@@ -20,8 +20,8 @@ export type QueriesState = {
 
 type SetNewTransactionsAction = {
   type: typeof constants.SET_NEW_TRANSACTIONS;
-  newTransactions?: unknown[];
-  matchedTransactions?: unknown[];
+  newTransactions?: string[];
+  matchedTransactions?: string[];
   updatedAccounts?: string[];
 };
 
diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts
index bacb94ca5..55fbda9c4 100644
--- a/packages/loot-core/src/types/models/rule.d.ts
+++ b/packages/loot-core/src/types/models/rule.d.ts
@@ -55,8 +55,9 @@ type BaseConditionEntity<
     year?: boolean;
   };
   conditionsOp?: string;
-  type?: 'id' | 'boolean' | 'date' | 'number';
+  type?: 'id' | 'boolean' | 'date' | 'number' | 'string';
   customName?: string;
+  queryFilter?: Record<string, { $oneof: string[] }>;
 };
 
 export type RuleConditionEntity =
diff --git a/packages/loot-core/src/types/models/transaction-filter.d.ts b/packages/loot-core/src/types/models/transaction-filter.d.ts
index e43d50e21..c688cc6fb 100644
--- a/packages/loot-core/src/types/models/transaction-filter.d.ts
+++ b/packages/loot-core/src/types/models/transaction-filter.d.ts
@@ -1,7 +1,9 @@
+import { type RuleConditionEntity } from './rule';
+
 export interface TransactionFilterEntity {
   id: string;
   name: string;
-  conditions_op: string;
-  conditions: unknown;
+  conditionsOp: 'and' | 'or';
+  conditions: RuleConditionEntity[];
   tombstone: boolean;
 }
diff --git a/packages/loot-core/src/types/models/transaction.d.ts b/packages/loot-core/src/types/models/transaction.d.ts
index 9cb097fec..648eec974 100644
--- a/packages/loot-core/src/types/models/transaction.d.ts
+++ b/packages/loot-core/src/types/models/transaction.d.ts
@@ -24,4 +24,6 @@ export interface TransactionEntity {
   tombstone?: boolean;
   schedule?: ScheduleEntity['id'];
   subtransactions?: TransactionEntity[];
+  _unmatched?: boolean;
+  _deleted?: boolean;
 }
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index 29f348631..a9dd5235c 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -24,6 +24,7 @@ export type SyncedPrefs = Partial<
     numberFormat: (typeof numberFormats)[number]['value'];
     hideFraction: boolean;
     isPrivacyEnabled: boolean;
+    [key: `show-balances-${string}`]: boolean;
     [key: `show-extra-balances-${string}`]: boolean;
     [key: `hide-cleared-${string}`]: boolean;
     [key: `hide-reconciled-${string}`]: boolean;
diff --git a/upcoming-release-notes/3311.md b/upcoming-release-notes/3311.md
new file mode 100644
index 000000000..c7eabab26
--- /dev/null
+++ b/upcoming-release-notes/3311.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [MatissJanis]
+---
+
+TypeScript: migrate Account component.
diff --git a/yarn.lock b/yarn.lock
index 179f6284b..ed7ca65ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -73,6 +73,7 @@ __metadata:
     "@swc/plugin-react-remove-properties": "npm:^1.5.121"
     "@testing-library/react": "npm:14.1.2"
     "@testing-library/user-event": "npm:14.5.2"
+    "@types/debounce": "npm:^1.2.4"
     "@types/lodash": "npm:^4"
     "@types/promise-retry": "npm:^1.1.6"
     "@types/react": "npm:^18.2.0"
@@ -5031,6 +5032,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/debounce@npm:^1.2.4":
+  version: 1.2.4
+  resolution: "@types/debounce@npm:1.2.4"
+  checksum: 10/decef3eee65d681556d50f7fac346f1b33134f6b21f806d41326f9dfb362fa66b0282ff0640ae6791b690694c9dc3dad4e146e909e707e6f96650f3aa325b9da
+  languageName: node
+  linkType: hard
+
 "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6":
   version: 4.1.8
   resolution: "@types/debug@npm:4.1.8"
-- 
GitLab