diff --git a/packages/desktop-client/src/components/sidebar/Account.tsx b/packages/desktop-client/src/components/sidebar/Account.tsx
index b882d1cf8b23ba3b24e090bc4e4aca80a9be4203..28c4420a8aaabfe136ec65bab1f1c6babfae658f 100644
--- a/packages/desktop-client/src/components/sidebar/Account.tsx
+++ b/packages/desktop-client/src/components/sidebar/Account.tsx
@@ -21,7 +21,7 @@ import {
   type OnDragChangeCallback,
   type OnDropCallback,
 } from '../sort';
-import { type Binding } from '../spreadsheet';
+import { type SheetFields, type Binding } from '../spreadsheet';
 import { CellValue } from '../spreadsheet/CellValue';
 
 export const accountNameStyle: CSSProperties = {
@@ -37,10 +37,10 @@ export const accountNameStyle: CSSProperties = {
   ...styles.smallText,
 };
 
-type AccountProps = {
+type AccountProps<FieldName extends SheetFields<'account'>> = {
   name: string;
   to: string;
-  query: Binding;
+  query: Binding<'account', FieldName>;
   account?: AccountEntity;
   connected?: boolean;
   pending?: boolean;
@@ -52,7 +52,7 @@ type AccountProps = {
   onDrop?: OnDropCallback;
 };
 
-export function Account({
+export function Account<FieldName extends SheetFields<'account'>>({
   name,
   account,
   connected,
@@ -65,7 +65,7 @@ export function Account({
   outerStyle,
   onDragChange,
   onDrop,
-}: AccountProps) {
+}: AccountProps<FieldName>) {
   const type = account
     ? account.closed
       ? 'account-closed'
diff --git a/packages/desktop-client/src/components/spreadsheet/index.ts b/packages/desktop-client/src/components/spreadsheet/index.ts
index 0a069c8c65f3778668b07faa26e544fde3e3039c..5e3df0052befc3eae6c7d271b75f94f958110e4d 100644
--- a/packages/desktop-client/src/components/spreadsheet/index.ts
+++ b/packages/desktop-client/src/components/spreadsheet/index.ts
@@ -1,4 +1,40 @@
-// @ts-strict-ignore
 import { type Query } from 'loot-core/src/shared/query';
 
-export type Binding = string | { name: string; value?; query?: Query };
+export type Spreadsheets = {
+  account: {
+    // Common fields
+    'uncategorized-amount': number;
+    'uncategorized-balance': number;
+
+    // Account fields
+    balance: number;
+    'accounts-balance': number;
+    'budgeted-accounts-balance': number;
+    'offbudget-accounts-balance': number;
+    balanceCleared: number;
+    balanceUncleared: number;
+  };
+};
+
+export type SheetNames = keyof Spreadsheets & string;
+
+export type SheetFields<SheetName extends SheetNames> =
+  keyof Spreadsheets[SheetName] & string;
+
+export type Binding<
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  SheetName extends SheetNames = any,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  SheetFieldName extends SheetFields<SheetName> = any,
+> =
+  | SheetFieldName
+  | {
+      name: SheetFieldName;
+      value?: Spreadsheets[SheetName][SheetFieldName];
+      query?: Query;
+    };
+export const parametrizedField =
+  <SheetName extends SheetNames>() =>
+  <SheetFieldName extends SheetFields<SheetName>>(field: SheetFieldName) =>
+  (id: string): SheetFieldName =>
+    `${field}-${id}` as SheetFieldName;
diff --git a/packages/desktop-client/src/components/table.tsx b/packages/desktop-client/src/components/table.tsx
index 7a7002410bd2b6a0bfa5750e7b12f222893941b3..0d3317d9a8ddcded24ede6492e8cf4c92a274b12 100644
--- a/packages/desktop-client/src/components/table.tsx
+++ b/packages/desktop-client/src/components/table.tsx
@@ -40,7 +40,12 @@ import {
   ConditionalPrivacyFilter,
   mergeConditionalPrivacyFilterProps,
 } from './PrivacyFilter';
-import { type Binding } from './spreadsheet';
+import {
+  type Spreadsheets,
+  type SheetFields,
+  type SheetNames,
+  type Binding,
+} from './spreadsheet';
 import { type FormatType, useFormat } from './spreadsheet/useFormat';
 import { useSheetValue } from './spreadsheet/useSheetValue';
 
@@ -311,7 +316,7 @@ const readonlyInputStyle = {
   '::selection': { backgroundColor: theme.formInputTextReadOnlySelection },
 };
 
-type InputValueProps = ComponentProps<typeof Input> & {
+type InputValueProps = Omit<ComponentProps<typeof Input>, 'value'> & {
   value?: string;
 };
 
@@ -671,31 +676,47 @@ export function SelectCell({
   );
 }
 
-type SheetCellValueProps = {
-  binding: Binding;
+type SheetCellValueProps<
+  SheetName extends SheetNames,
+  FieldName extends SheetFields<SheetName>,
+> = {
+  binding: Binding<SheetName, FieldName>;
   type: FormatType;
-  getValueStyle?: (value: string | number) => CSSProperties;
-  formatExpr?: (value) => string;
+  getValueStyle?: (value: Spreadsheets[SheetName][FieldName]) => CSSProperties;
+  formatExpr?: (value: Spreadsheets[SheetName][FieldName]) => string;
   unformatExpr?: (value: string) => unknown;
   privacyFilter?: ComponentProps<
     typeof ConditionalPrivacyFilter
   >['privacyFilter'];
 };
 
-type SheetCellProps = ComponentProps<typeof Cell> & {
-  valueProps: SheetCellValueProps;
-  inputProps?: Omit<ComponentProps<typeof InputValue>, 'value' | 'onUpdate'>;
+type SheetCellProps<
+  SheetName extends SheetNames,
+  FieldName extends SheetFields<SheetName>,
+> = ComponentProps<typeof Cell> & {
+  valueProps: SheetCellValueProps<SheetName, FieldName>;
+  inputProps?: Omit<
+    ComponentProps<typeof InputValue>,
+    'value' | 'onUpdate' | 'onBlur'
+  > & {
+    onBlur?: () => void;
+  };
   onSave?: (value) => void;
   textAlign?: CSSProperties['textAlign'];
 };
-export function SheetCell({
+export function SheetCell<
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  SheetName extends SheetNames = any,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  FieldName extends SheetFields<SheetName> = any,
+>({
   valueProps,
   valueStyle,
   inputProps,
   textAlign,
   onSave,
   ...props
-}: SheetCellProps) {
+}: SheetCellProps<SheetName, FieldName>) {
   const {
     binding,
     type,
@@ -705,10 +726,10 @@ export function SheetCell({
     privacyFilter,
   } = valueProps;
 
-  const sheetValue = useSheetValue(binding, e => {
+  const sheetValue = useSheetValue(binding, () => {
     // "close" the cell if it's editing
     if (props.exposed && inputProps && inputProps.onBlur) {
-      inputProps.onBlur(e);
+      inputProps.onBlur();
     }
   });
   const format = useFormat();
@@ -722,7 +743,7 @@ export function SheetCell({
       }
       textAlign={textAlign}
       {...props}
-      value={sheetValue}
+      value={String(sheetValue ?? '')}
       formatter={value =>
         props.formatter ? props.formatter(value, type) : format(value, type)
       }
@@ -738,7 +759,7 @@ export function SheetCell({
       {() => {
         return (
           <InputValue
-            value={formatExpr ? formatExpr(sheetValue) : sheetValue}
+            value={formatExpr ? formatExpr(sheetValue) : sheetValue.toString()}
             onUpdate={value => {
               onSave(unformatExpr ? unformatExpr(value) : value);
             }}
diff --git a/packages/loot-core/src/client/queries.ts b/packages/loot-core/src/client/queries.ts
index 06279fd33609bd5b5ada5eb14aaca22fbe89694c..3c6402249a93dc556268f23d93f7829a12f8234c 100644
--- a/packages/loot-core/src/client/queries.ts
+++ b/packages/loot-core/src/client/queries.ts
@@ -1,6 +1,11 @@
 // @ts-strict-ignore
 import { parse as parseDate, isValid as isDateValid } from 'date-fns';
 
+import {
+  parametrizedField,
+  type Binding,
+  type SheetNames,
+} from '../../../desktop-client/src/components/spreadsheet';
 import {
   dayFromDate,
   getDayMonthRegex,
@@ -8,10 +13,14 @@ import {
   getShortYearRegex,
   getShortYearFormat,
 } from '../shared/months';
-import { q } from '../shared/query';
+import { q, type Query } from '../shared/query';
 import { currencyToAmount, amountToInteger } from '../shared/util';
+import { type CategoryEntity, type AccountEntity } from '../types/models';
+import { type LocalPrefs } from '../types/prefs';
+
+const accountParametrizedField = parametrizedField<'account'>();
 
-export function getAccountFilter(accountId, field = 'account') {
+export function getAccountFilter(accountId: string, field = 'account') {
   if (accountId) {
     if (accountId === 'budgeted') {
       return {
@@ -47,7 +56,7 @@ export function getAccountFilter(accountId, field = 'account') {
   return null;
 }
 
-export function makeTransactionsQuery(accountId) {
+export function makeTransactionsQuery(accountId: string) {
   let query = q('transactions').options({ splits: 'grouped' });
 
   const filter = getAccountFilter(accountId);
@@ -58,7 +67,11 @@ export function makeTransactionsQuery(accountId) {
   return query;
 }
 
-export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
+export function makeTransactionSearchQuery(
+  currentQuery: Query,
+  search: string,
+  dateFormat: LocalPrefs['dateFormat'],
+) {
   const amount = currencyToAmount(search);
 
   // Support various date formats
@@ -94,9 +107,11 @@ export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
   });
 }
 
-export function accountBalance(acct) {
+export function accountBalance(
+  acct: AccountEntity,
+): Binding<'account', 'balance'> {
   return {
-    name: `balance-${acct.id}`,
+    name: accountParametrizedField('balance')(acct.id),
     query: q('transactions')
       .filter({ account: acct.id })
       .options({ splits: 'none' })
@@ -104,9 +119,11 @@ export function accountBalance(acct) {
   };
 }
 
-export function accountBalanceCleared(acct) {
+export function accountBalanceCleared(
+  acct: AccountEntity,
+): Binding<'account', 'balanceCleared'> {
   return {
-    name: `balanceCleared-${acct.id}`,
+    name: accountParametrizedField('balanceCleared')(acct.id),
     query: q('transactions')
       .filter({ account: acct.id, cleared: true })
       .options({ splits: 'none' })
@@ -114,9 +131,11 @@ export function accountBalanceCleared(acct) {
   };
 }
 
-export function accountBalanceUncleared(acct) {
+export function accountBalanceUncleared(
+  acct: AccountEntity,
+): Binding<'account', 'balanceUncleared'> {
   return {
-    name: `balanceUncleared-${acct.id}`,
+    name: accountParametrizedField('balanceUncleared')(acct.id),
     query: q('transactions')
       .filter({ account: acct.id, cleared: false })
       .options({ splits: 'none' })
@@ -124,7 +143,7 @@ export function accountBalanceUncleared(acct) {
   };
 }
 
-export function allAccountBalance() {
+export function allAccountBalance(): Binding<'account', 'accounts-balance'> {
   return {
     query: q('transactions')
       .filter({ 'account.closed': false })
@@ -133,7 +152,10 @@ export function allAccountBalance() {
   };
 }
 
-export function budgetedAccountBalance() {
+export function budgetedAccountBalance(): Binding<
+  'account',
+  'budgeted-accounts-balance'
+> {
   return {
     name: `budgeted-accounts-balance`,
     query: q('transactions')
@@ -142,7 +164,10 @@ export function budgetedAccountBalance() {
   };
 }
 
-export function offbudgetAccountBalance() {
+export function offbudgetAccountBalance(): Binding<
+  'account',
+  'offbudget-accounts-balance'
+> {
   return {
     name: `offbudget-accounts-balance`,
     query: q('transactions')
@@ -151,7 +176,7 @@ export function offbudgetAccountBalance() {
   };
 }
 
-export function categoryBalance(category, month) {
+export function categoryBalance(category: CategoryEntity, month: string) {
   return {
     name: `balance-${category.id}`,
     query: q('transactions')
@@ -164,7 +189,10 @@ export function categoryBalance(category, month) {
   };
 }
 
-export function categoryBalanceCleared(category, month) {
+export function categoryBalanceCleared(
+  category: CategoryEntity,
+  month: string,
+) {
   return {
     name: `balanceCleared-${category.id}`,
     query: q('transactions')
@@ -178,7 +206,10 @@ export function categoryBalanceCleared(category, month) {
   };
 }
 
-export function categoryBalanceUncleared(category, month) {
+export function categoryBalanceUncleared(
+  category: CategoryEntity,
+  month: string,
+) {
   return {
     name: `balanceUncleared-${category.id}`,
     query: q('transactions')
@@ -210,7 +241,10 @@ export function uncategorizedBalance() {
   };
 }
 
-export function uncategorizedCount() {
+export function uncategorizedCount<SheetName extends SheetNames>(): Binding<
+  SheetName,
+  'uncategorized-amount'
+> {
   return {
     name: 'uncategorized-amount',
     query: uncategorizedQuery.calculate({ $count: '$id' }),
diff --git a/upcoming-release-notes/3093.md b/upcoming-release-notes/3093.md
new file mode 100644
index 0000000000000000000000000000000000000000..10a21f26eccef50c123b1e855f7971fab3ed4178
--- /dev/null
+++ b/upcoming-release-notes/3093.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [jfdoming]
+---
+
+Support type-checking on spreadsheet fields (part 1)