diff --git a/packages/desktop-client/src/components/Page.js b/packages/desktop-client/src/components/Page.tsx
similarity index 78%
rename from packages/desktop-client/src/components/Page.js
rename to packages/desktop-client/src/components/Page.tsx
index 8038a87db5cd893b767c3bd855d1e023757dea6a..aa2d8999d5cb184aa7e8a2527c394def5b114f4e 100644
--- a/packages/desktop-client/src/components/Page.js
+++ b/packages/desktop-client/src/components/Page.tsx
@@ -1,12 +1,14 @@
-import React, { createContext, useContext } from 'react';
+import React, { createContext, type ReactNode, useContext } from 'react';
 import { useNavigate } from 'react-router-dom';
 
+import { type CSSProperties } from 'glamor';
+
 import { useResponsive } from '../ResponsiveProvider';
 import { colors, styles } from '../style';
 
 import { Modal, View, Text } from './common';
 
-let PageTypeContext = createContext({ type: 'page' });
+let PageTypeContext = createContext({ type: 'page', current: undefined });
 
 export function PageTypeProvider({ type, current, children }) {
   return (
@@ -63,18 +65,29 @@ function PageTitle({ name, style }) {
   );
 }
 
-export function Page({ title, modalSize, children, titleStyle }) {
+export function Page({
+  title,
+  modalSize,
+  children,
+  titleStyle,
+}: {
+  title: string;
+  modalSize?: string | { width: number; height?: number };
+  children: ReactNode;
+  titleStyle?: CSSProperties;
+}) {
   let { type, current } = usePageType();
   let navigate = useNavigate();
   let { isNarrowWidth } = useResponsive();
   let HORIZONTAL_PADDING = isNarrowWidth ? 10 : 20;
 
   if (type === 'modal') {
-    let size = modalSize;
-    if (typeof modalSize === 'string') {
-      size =
-        modalSize === 'medium' ? { width: 750, height: 600 } : { width: 600 };
-    }
+    let size =
+      typeof modalSize === 'string'
+        ? modalSize === 'medium'
+          ? { width: 750, height: 600 }
+          : { width: 600 }
+        : modalSize;
 
     return (
       <Modal
diff --git a/packages/desktop-client/src/components/common/Select.tsx b/packages/desktop-client/src/components/common/Select.tsx
index f328d8479a0669ed9ecb8aed9f69dfc48280fb7a..cbc01a993349e2423162c9f9277f36f54748cf62 100644
--- a/packages/desktop-client/src/components/common/Select.tsx
+++ b/packages/desktop-client/src/components/common/Select.tsx
@@ -10,15 +10,15 @@ import { type CSSProperties, css } from 'glamor';
 import ExpandArrow from '../../icons/v0/ExpandArrow';
 import { colors } from '../../style';
 
-type CustomSelectProps = {
-  options: Array<[string, string]>;
-  value: string;
+type SelectProps<Value extends string> = {
+  options: Array<[Value, string]>;
+  value: Value;
   defaultLabel?: string;
-  onChange?: (newValue: string) => void;
+  onChange?: (newValue: Value) => void;
   style?: CSSProperties;
   wrapperStyle?: CSSProperties;
-  line: number;
-  disabledKeys?: string[];
+  line?: number;
+  disabledKeys?: Value[];
 };
 
 /**
@@ -36,7 +36,7 @@ type CustomSelectProps = {
  * // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="3" defaultLabel="Select an option"  onChange={handleOnChange} />
  */
 
-export default function Select({
+export default function Select<Value extends string>({
   options,
   value,
   defaultLabel = '',
@@ -45,7 +45,7 @@ export default function Select({
   wrapperStyle,
   line,
   disabledKeys = [],
-}: CustomSelectProps) {
+}: SelectProps<Value>) {
   const arrowSize = 7;
   const targetOption = options.filter(option => option[0] === value);
   return (
diff --git a/packages/desktop-client/src/components/settings/Encryption.js b/packages/desktop-client/src/components/settings/Encryption.tsx
similarity index 97%
rename from packages/desktop-client/src/components/settings/Encryption.js
rename to packages/desktop-client/src/components/settings/Encryption.tsx
index bbedaa05292666abf2712a7cf5f0340eb7cd0b84..4f419ee5cdabe1955819aa9b20e1b992bc6f1aef 100644
--- a/packages/desktop-client/src/components/settings/Encryption.js
+++ b/packages/desktop-client/src/components/settings/Encryption.tsx
@@ -16,6 +16,7 @@ export default function EncryptionSettings() {
   const missingCryptoAPI = !(window.crypto && crypto.subtle);
 
   function onChangeKey() {
+    // @ts-expect-error useActions() type does not properly handle overloads
     pushModal('create-encryption-key', { recreate: true });
   }
 
diff --git a/packages/desktop-client/src/components/settings/Experimental.tsx b/packages/desktop-client/src/components/settings/Experimental.tsx
index d0fe855623ac1067e7dcefd2afa29ab580f0dc47..0b6d1908a661721330c8b71b81abb34535f7a78d 100644
--- a/packages/desktop-client/src/components/settings/Experimental.tsx
+++ b/packages/desktop-client/src/components/settings/Experimental.tsx
@@ -32,6 +32,7 @@ function FeatureToggle({
       <Checkbox
         checked={enabled}
         onChange={() => {
+          // @ts-expect-error key type is not correctly inferred
           savePrefs({
             [`flags.${flag}`]: !enabled,
           });
diff --git a/packages/desktop-client/src/components/settings/Export.js b/packages/desktop-client/src/components/settings/Export.tsx
similarity index 94%
rename from packages/desktop-client/src/components/settings/Export.js
rename to packages/desktop-client/src/components/settings/Export.tsx
index 1417b5ff37747f3e096040afe3231b9b86895f65..bc966988c4ccf79ee61c446b839e959dd4ba9b23 100644
--- a/packages/desktop-client/src/components/settings/Export.js
+++ b/packages/desktop-client/src/components/settings/Export.tsx
@@ -10,7 +10,7 @@ import { Text, Button } from '../common';
 import { Setting } from './UI';
 
 export default function ExportBudget() {
-  let budgetId = useSelector(state => state.prefs.local.budgetId);
+  let budgetId = useSelector(state => state.prefs.local.id);
   let encryptKeyId = useSelector(state => state.prefs.local.encryptKeyId);
 
   async function onExport() {
diff --git a/packages/desktop-client/src/components/settings/FixSplits.js b/packages/desktop-client/src/components/settings/FixSplits.tsx
similarity index 92%
rename from packages/desktop-client/src/components/settings/FixSplits.js
rename to packages/desktop-client/src/components/settings/FixSplits.tsx
index fa3eeb0db10ced059ae4088a481395f8761b5bc4..28892095fd2bbf2dabc3bface6e0f4564441fb85 100644
--- a/packages/desktop-client/src/components/settings/FixSplits.js
+++ b/packages/desktop-client/src/components/settings/FixSplits.tsx
@@ -1,6 +1,7 @@
 import React, { useState } from 'react';
 
 import { send } from 'loot-core/src/platform/client/fetch';
+import { type Handlers } from 'loot-core/src/types/handlers';
 
 import { colors } from '../../style';
 import { View, Text, ButtonWithLoading } from '../common';
@@ -8,7 +9,9 @@ import Paragraph from '../common/Paragraph';
 
 import { Setting } from './UI';
 
-function renderResults(results) {
+type Results = Awaited<ReturnType<Handlers['tools/fix-split-transactions']>>;
+
+function renderResults(results: Results) {
   let { numBlankPayees, numCleared, numDeleted } = results;
   let result = '';
   if (numBlankPayees === 0 && numCleared === 0 && numDeleted === 0) {
@@ -48,7 +51,7 @@ function renderResults(results) {
 
 export default function FixSplitsTool() {
   let [loading, setLoading] = useState(false);
-  let [results, setResults] = useState(null);
+  let [results, setResults] = useState<Results>(null);
 
   async function onFix() {
     setLoading(true);
diff --git a/packages/desktop-client/src/components/settings/Format.js b/packages/desktop-client/src/components/settings/Format.tsx
similarity index 83%
rename from packages/desktop-client/src/components/settings/Format.js
rename to packages/desktop-client/src/components/settings/Format.tsx
index c92034ed75f945753f8e64f6116a69602ef263ad..666e330c93be39a79cf818fb0a9375d396c440a8 100644
--- a/packages/desktop-client/src/components/settings/Format.js
+++ b/packages/desktop-client/src/components/settings/Format.tsx
@@ -1,7 +1,8 @@
-import React from 'react';
+import React, { type ReactNode } from 'react';
 import { useSelector } from 'react-redux';
 
 import { numberFormats } from 'loot-core/src/shared/util';
+import { type LocalPrefs } from 'loot-core/src/types/prefs';
 
 import { useActions } from '../../hooks/useActions';
 import tokens from '../../tokens';
@@ -13,7 +14,7 @@ import { Setting } from './UI';
 
 // Follows Pikaday 'firstDay' numbering
 // https://github.com/Pikaday/Pikaday
-let daysOfWeek = [
+let daysOfWeek: { value: LocalPrefs['firstDayOfWeekIdx']; label: string }[] = [
   { value: '0', label: 'Sunday' },
   { value: '1', label: 'Monday' },
   { value: '2', label: 'Tuesday' },
@@ -23,7 +24,7 @@ let daysOfWeek = [
   { value: '6', label: 'Saturday' },
 ];
 
-let dateFormats = [
+let dateFormats: { value: LocalPrefs['dateFormat']; label: string }[] = [
   { value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' },
   { value: 'dd/MM/yyyy', label: 'DD/MM/YYYY' },
   { value: 'yyyy-MM-dd', label: 'YYYY-MM-DD' },
@@ -31,7 +32,7 @@ let dateFormats = [
   { value: 'dd.MM.yyyy', label: 'DD.MM.YYYY' },
 ];
 
-function Column({ title, children }) {
+function Column({ title, children }: { title: string; children: ReactNode }) {
   return (
     <View
       style={{
@@ -50,23 +51,6 @@ function Column({ title, children }) {
 export default function FormatSettings() {
   let { savePrefs } = useActions();
 
-  function onFirstDayOfWeek(idx) {
-    savePrefs({ firstDayOfWeekIdx: idx });
-  }
-
-  function onDateFormat(format) {
-    savePrefs({ dateFormat: format });
-  }
-
-  function onNumberFormat(format) {
-    savePrefs({ numberFormat: format });
-  }
-
-  function onHideFraction(e) {
-    let hideFraction = e.target.checked;
-    savePrefs({ hideFraction });
-  }
-
   let sidebar = useSidebar();
   let firstDayOfWeekIdx = useSelector(
     state => state.prefs.local.firstDayOfWeekIdx || '0', // Sunday
@@ -99,9 +83,9 @@ export default function FormatSettings() {
           <Column title="Numbers">
             <Button bounce={false} style={{ padding: 0 }}>
               <Select
-                key={hideFraction} // needed because label does not update
+                key={String(hideFraction)} // needed because label does not update
                 value={numberFormat}
-                onChange={onNumberFormat}
+                onChange={format => savePrefs({ numberFormat: format })}
                 options={numberFormats.map(f => [
                   f.value,
                   hideFraction ? f.labelNoFraction : f.label,
@@ -114,7 +98,9 @@ export default function FormatSettings() {
               <Checkbox
                 id="settings-textDecimal"
                 checked={!!hideFraction}
-                onChange={onHideFraction}
+                onChange={e =>
+                  savePrefs({ hideFraction: e.currentTarget.checked })
+                }
               />
               <label htmlFor="settings-textDecimal">Hide decimal places</label>
             </Text>
@@ -124,7 +110,7 @@ export default function FormatSettings() {
             <Button bounce={false} style={{ padding: 0 }}>
               <Select
                 value={dateFormat}
-                onChange={onDateFormat}
+                onChange={format => savePrefs({ dateFormat: format })}
                 options={dateFormats.map(f => [f.value, f.label])}
                 style={{ padding: '2px 10px', fontSize: 15 }}
               />
@@ -135,7 +121,7 @@ export default function FormatSettings() {
             <Button bounce={false} style={{ padding: 0 }}>
               <Select
                 value={firstDayOfWeekIdx}
-                onChange={onFirstDayOfWeek}
+                onChange={idx => savePrefs({ firstDayOfWeekIdx: idx })}
                 options={daysOfWeek.map(f => [f.value, f.label])}
                 style={{ padding: '2px 10px', fontSize: 15 }}
               />
diff --git a/packages/desktop-client/src/components/settings/Global.js b/packages/desktop-client/src/components/settings/Global.tsx
similarity index 97%
rename from packages/desktop-client/src/components/settings/Global.js
rename to packages/desktop-client/src/components/settings/Global.tsx
index 318332e8c88c252c30228266eddf3d108851ec3d..1ea218976b204831eb714ea6b5db8c0bfda8f36c 100644
--- a/packages/desktop-client/src/components/settings/Global.js
+++ b/packages/desktop-client/src/components/settings/Global.tsx
@@ -13,7 +13,7 @@ export default function GlobalSettings() {
   let { saveGlobalPrefs } = useActions();
 
   let [documentDirChanged, setDirChanged] = useState(false);
-  let dirScrolled = useRef(null);
+  let dirScrolled = useRef<HTMLSpanElement>(null);
 
   useEffect(() => {
     if (dirScrolled.current) {
diff --git a/packages/desktop-client/src/components/settings/Reset.js b/packages/desktop-client/src/components/settings/Reset.tsx
similarity index 100%
rename from packages/desktop-client/src/components/settings/Reset.js
rename to packages/desktop-client/src/components/settings/Reset.tsx
diff --git a/packages/desktop-client/src/components/settings/Themes.js b/packages/desktop-client/src/components/settings/Themes.tsx
similarity index 100%
rename from packages/desktop-client/src/components/settings/Themes.js
rename to packages/desktop-client/src/components/settings/Themes.tsx
diff --git a/packages/desktop-client/src/components/settings/UI.tsx b/packages/desktop-client/src/components/settings/UI.tsx
index d5f9bdd9cab489bcb3593381b23d38c97c8bb7cc..b3052256e1d432874fb4858bfd3c2200bde7c041 100644
--- a/packages/desktop-client/src/components/settings/UI.tsx
+++ b/packages/desktop-client/src/components/settings/UI.tsx
@@ -9,7 +9,7 @@ import tokens from '../../tokens';
 import { View, LinkButton } from '../common';
 
 type SettingProps = {
-  primaryAction: ReactNode;
+  primaryAction?: ReactNode;
   style?: CSSProperties;
   children: ReactNode;
 };
diff --git a/packages/desktop-client/src/components/settings/index.js b/packages/desktop-client/src/components/settings/index.tsx
similarity index 97%
rename from packages/desktop-client/src/components/settings/index.js
rename to packages/desktop-client/src/components/settings/index.tsx
index e0571dd2d1619f7e0958a9b47ed57f849bfe49bc..def408102834e6f0798ea5db24dae059d44e0d65 100644
--- a/packages/desktop-client/src/components/settings/index.js
+++ b/packages/desktop-client/src/components/settings/index.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React, { type ReactNode, useEffect } from 'react';
 import { useSelector } from 'react-redux';
 
 import { media } from 'glamor';
@@ -75,7 +75,7 @@ function About() {
   );
 }
 
-function IDName({ children }) {
+function IDName({ children }: { children: ReactNode }) {
   return <Text style={{ fontWeight: 500 }}>{children}</Text>;
 }
 
diff --git a/packages/loot-core/src/client/actions/modals.ts b/packages/loot-core/src/client/actions/modals.ts
index bcb71af529e7fc74b9958afb7b7ff14aabe51214..efc1a0ae124d05d6787f1d2721b8c76da4830673 100644
--- a/packages/loot-core/src/client/actions/modals.ts
+++ b/packages/loot-core/src/client/actions/modals.ts
@@ -1,24 +1,37 @@
 import * as constants from '../constants';
 import type {
+  OptionlessModal,
   CloseModalAction,
-  Modal,
   PopModalAction,
   PushModalAction,
   ReplaceModalAction,
+  ModalWithOptions,
+  ModalType,
+  FinanceModals,
 } from '../state-types/modals';
 
-export function pushModal<M extends Modal>(
-  name: M['name'],
-  options: M['options'],
+export function pushModal<M extends keyof ModalWithOptions>(
+  name: M,
+  options: ModalWithOptions[M],
+): PushModalAction;
+export function pushModal(name: OptionlessModal): PushModalAction;
+export function pushModal<M extends ModalType>(
+  name: M,
+  options?: FinanceModals[M],
 ): PushModalAction {
   // @ts-expect-error TS is unable to determine that `name` and `options` match
   let modal: M = { name, options };
   return { type: constants.PUSH_MODAL, modal };
 }
 
-export function replaceModal<M extends Modal>(
-  name: M['name'],
-  options: M['options'],
+export function replaceModal<M extends keyof ModalWithOptions>(
+  name: M,
+  options: ModalWithOptions[M],
+): ReplaceModalAction;
+export function replaceModal(name: OptionlessModal): ReplaceModalAction;
+export function replaceModal<M extends ModalType>(
+  name: M,
+  options?: FinanceModals[M],
 ): ReplaceModalAction {
   // @ts-expect-error TS is unable to determine that `name` and `options` match
   let modal: M = { name, options };
diff --git a/packages/loot-core/src/client/actions/prefs.ts b/packages/loot-core/src/client/actions/prefs.ts
index 3521e89aefd3a1ccb571577131cb19efd507a10b..d309e017327ff53e75a5d8c6b257b76a21543433 100644
--- a/packages/loot-core/src/client/actions/prefs.ts
+++ b/packages/loot-core/src/client/actions/prefs.ts
@@ -1,4 +1,5 @@
 import { send } from '../../platform/client/fetch';
+import type * as prefs from '../../types/prefs';
 import * as constants from '../constants';
 
 import { closeModal } from './modals';
@@ -24,7 +25,7 @@ export function loadPrefs() {
   };
 }
 
-export function savePrefs(prefs) {
+export function savePrefs(prefs: Partial<prefs.LocalPrefs>) {
   return async (dispatch: Dispatch) => {
     await send('save-prefs', prefs);
     dispatch({
@@ -46,7 +47,7 @@ export function loadGlobalPrefs() {
   };
 }
 
-export function saveGlobalPrefs(prefs) {
+export function saveGlobalPrefs(prefs: Partial<prefs.GlobalPrefs>) {
   return async (dispatch: Dispatch) => {
     await send('save-global-prefs', prefs);
     dispatch({
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 6065b00265e8a2f341eddf1ca61244d4ce9c887e..c4f1fac446d90a84ead792ee912c70361dc00345 100644
--- a/packages/loot-core/src/client/state-types/modals.d.ts
+++ b/packages/loot-core/src/client/state-types/modals.d.ts
@@ -1,13 +1,19 @@
 import type { AccountEntity } from '../../types/models';
 import type { RuleEntity } from '../../types/models/rule';
+import type { EmptyObject, StripNever } from '../../types/util';
 import type * as constants from '../constants';
 
-type Modal = {
-  [K in keyof FinanceModals]: {
-    name: K;
-    options: FinanceModals[K];
-  };
-}[keyof FinanceModals];
+export type ModalType = keyof FinanceModals;
+
+export type OptionlessModal = {
+  [K in ModalType]: EmptyObject extends FinanceModals[K] ? K : never;
+}[ModalType];
+
+export type ModalWithOptions = StripNever<{
+  [K in ModalType]: keyof FinanceModals[K] extends never
+    ? never
+    : FinanceModals[K];
+}>;
 
 // There is a separate (overlapping!) set of modals for the management app. Fun!
 type FinanceModals = {
@@ -17,8 +23,8 @@ type FinanceModals = {
     onImported: (didChange: boolean) => void;
   };
 
-  'add-account': null;
-  'add-local-account': null;
+  'add-account': EmptyObject;
+  'add-local-account': EmptyObject;
   'close-account': {
     account: AccountEntity;
     balance: number;
@@ -29,16 +35,15 @@ type FinanceModals = {
     requisitionId: string;
     upgradingAccountId: string;
   };
-  'configure-linked-accounts': never;
 
   'confirm-category-delete': { onDelete: () => void } & (
     | { category: string }
     | { group: string }
   );
 
-  'load-backup': null;
+  'load-backup': EmptyObject;
 
-  'manage-rules': { payeeId: string } | null;
+  'manage-rules': { payeeId?: string };
   'edit-rule': {
     rule: RuleEntity;
     onSave: (rule: RuleEntity) => void;
@@ -65,10 +70,10 @@ type FinanceModals = {
     onSuccess: (data: unknown) => Promise<void>;
   };
 
-  'create-encryption-key': { recreate: boolean } | null;
+  'create-encryption-key': { recreate?: boolean };
   'fix-encryption-key': {
-    hasExistingKey: boolean;
-    cloudFileId: string;
+    hasExistingKey?: boolean;
+    cloudFileId?: string;
     onSuccess?: () => void;
   };
 
diff --git a/packages/loot-core/src/server/main.ts b/packages/loot-core/src/server/main.ts
index e070e1a2e8a7e5861d7edb6532b9a4ee025aae66..83e8e6f961894dbab258ef99f4b19683f1552878 100644
--- a/packages/loot-core/src/server/main.ts
+++ b/packages/loot-core/src/server/main.ts
@@ -1903,9 +1903,9 @@ handlers['download-budget'] = async function ({ fileId }) {
 // open and sync, but don’t close
 handlers['sync-budget'] = async function () {
   setSyncingMode('enabled');
-  await initialFullSync();
+  let result = await initialFullSync();
 
-  return {};
+  return result;
 };
 
 handlers['load-budget'] = async function ({ id }) {
diff --git a/packages/loot-core/src/server/sync/index.ts b/packages/loot-core/src/server/sync/index.ts
index 34dceb7dd1ff988138db3ab39e83dc824a7d524f..114c337906b02e8e8b5c79d59fb92e152d0f543d 100644
--- a/packages/loot-core/src/server/sync/index.ts
+++ b/packages/loot-core/src/server/sync/index.ts
@@ -538,12 +538,16 @@ function getTablesFromMessages(messages: Message[]): string[] {
 // spreadsheet to finish any processing. This is useful if we want to
 // perform a full sync and wait for everything to finish, usually if
 // you're doing an initial sync before working with a file.
-export async function initialFullSync(): Promise<void> {
+export async function initialFullSync(): Promise<{
+  error?: { message: string; reason: string; meta: unknown };
+}> {
   let result = await fullSync();
   if (isError(result)) {
     // Make sure to wait for anything in the spreadsheet to process
     await sheet.waitOnSpreadsheet();
+    return result;
   }
+  return {};
 }
 
 export const fullSync = once(async function (): Promise<
diff --git a/packages/loot-core/src/types/prefs.d.ts b/packages/loot-core/src/types/prefs.d.ts
index 4dcd19128b5a0d5897fc4a3e530d4fbfd414184b..4906c326e9df8f54ff4980845e6c35dbac41d3b0 100644
--- a/packages/loot-core/src/types/prefs.d.ts
+++ b/packages/loot-core/src/types/prefs.d.ts
@@ -9,7 +9,12 @@ export type FeatureFlag =
 export type LocalPrefs = Partial<
   {
     firstDayOfWeekIdx: `${0 | 1 | 2 | 3 | 4 | 5 | 6}`;
-    dateFormat: string;
+    dateFormat:
+      | 'MM/dd/yyyy'
+      | 'dd/MM/yyyy'
+      | 'yyyy-MM-dd'
+      | 'MM.dd.yyyy'
+      | 'dd.MM.yyyy';
     numberFormat: (typeof numberFormats)[number]['value'];
     hideFraction: boolean;
     hideClosedAccounts: boolean;
diff --git a/packages/loot-core/src/types/server-handlers.d.ts b/packages/loot-core/src/types/server-handlers.d.ts
index 32b56f1c1206545776dcfac66342f6774c8a9a50..8ce437c00631f071a71cd0e80a108f4989fee14e 100644
--- a/packages/loot-core/src/types/server-handlers.d.ts
+++ b/packages/loot-core/src/types/server-handlers.d.ts
@@ -301,7 +301,9 @@ export interface ServerHandlers {
 
   'download-budget': (arg: { fileId; replace? }) => Promise<{ error; id }>;
 
-  'sync-budget': () => Promise<EmptyObject>;
+  'sync-budget': () => Promise<{
+    error?: { message: string; reason: string; meta: unknown };
+  }>;
 
   'load-budget': (arg: { id }) => Promise<{ error }>;
 
diff --git a/packages/loot-core/src/types/util.d.ts b/packages/loot-core/src/types/util.d.ts
index 253e96456b8eafdbb7e9e15e51b35409cad47b68..f478d90061fd1ff1b75e7307f5197d99b2f38f43 100644
--- a/packages/loot-core/src/types/util.d.ts
+++ b/packages/loot-core/src/types/util.d.ts
@@ -1 +1,5 @@
-export type EmptyObject = Record<string, never>;
+export type EmptyObject = Record<never, never>;
+
+export type StripNever<T> = {
+  [K in keyof T as T[K] extends never ? never : K]: T[K];
+};
diff --git a/packages/loot-core/typings/window.d.ts b/packages/loot-core/typings/window.d.ts
index 001ef57094cabe797c513caf3176e4ca2ede9716..00401f26741be289a3a20571b51055e84d880ded 100644
--- a/packages/loot-core/typings/window.d.ts
+++ b/packages/loot-core/typings/window.d.ts
@@ -8,6 +8,14 @@ declare global {
       IS_FAKE_WEB: boolean;
       ACTUAL_VERSION: string;
       openURLInBrowser: (url: string) => void;
+      saveFile: (
+        contents: Buffer,
+        filename: string,
+        dialogTitle: string,
+      ) => void;
+      openFileDialog: (
+        opts: Parameters<import('electron').Dialog['showOpenDialogSync']>[0],
+      ) => Promise<string[]>;
     };
 
     __navigate?: import('react-router').NavigateFunction;
diff --git a/upcoming-release-notes/1405.md b/upcoming-release-notes/1405.md
new file mode 100644
index 0000000000000000000000000000000000000000..cf194652c76cfc7bcb69a4c026b96cf062c3dbe0
--- /dev/null
+++ b/upcoming-release-notes/1405.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [j-f1]
+---
+
+Port the settings-related code to TypeScript