You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/01/06 18:53:04 UTC

(superset) branch master updated: chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript (#36760)

This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 9aff89c1b4 chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript (#36760)
9aff89c1b4 is described below

commit 9aff89c1b4b783c7cd8809ea8d46a273c12f6f4e
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Tue Jan 6 10:52:58 2026 -0800

    chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript (#36760)
    
    Co-authored-by: Claude Opus 4.5 <no...@anthropic.com>
---
 .../playwright/tests/experimental/README.md        |   7 +
 .../src/SqlLab/actions/{sqlLab.js => sqlLab.ts}    | 794 +++++++++++++++------
 .../AceEditorWrapper/useKeywords.test.ts           |   6 +-
 .../components/AceEditorWrapper/useKeywords.ts     |   6 +-
 .../SqlLab/components/QueryAutoRefresh/index.tsx   |   2 +-
 .../SqlLab/components/SaveDatasetModal/index.tsx   |   6 +-
 .../src/SqlLab/components/SouthPane/index.tsx      |   4 +-
 .../src/SqlLab/components/SqlEditor/index.tsx      |   9 +-
 .../SqlLab/components/SqlEditorLeftBar/index.tsx   |   5 +-
 .../src/SqlLab/components/TableElement/index.tsx   |   2 +-
 .../middlewares/persistSqlLabStateEnhancer.js      | 136 ----
 .../middlewares/persistSqlLabStateEnhancer.ts      | 185 +++++
 .../src/SqlLab/reducers/{sqlLab.js => sqlLab.ts}   | 333 +++++----
 superset-frontend/src/core/sqlLab/index.ts         |  55 +-
 .../FiltersConfigForm/FiltersConfigForm.tsx        |   4 +-
 .../AnnotationLayer.test.tsx                       |  39 +-
 .../{AnnotationLayer.jsx => AnnotationLayer.tsx}   | 417 +++++++----
 .../{AnnotationTypes.js => AnnotationTypes.ts}     |  40 +-
 ...undsControl.test.jsx => BoundsControl.test.tsx} |   0
 .../{CheckboxControl.jsx => CheckboxControl.tsx}   |  52 +-
 .../CollectionControl/{index.jsx => index.tsx}     |  90 ++-
 .../DatasourceControl/DatasourceControl.test.tsx   | 110 +--
 .../DatasourceControl/{index.jsx => index.tsx}     | 157 +++-
 .../DndColumnMetricSelect.tsx                      |  15 +-
 ...erTitle.jsx => DndColumnSelectPopoverTitle.tsx} |  11 +-
 .../DndFilterSelect.test.tsx                       |  14 +-
 .../DndColumnSelectControl/DndMetricSelect.tsx     |  23 +-
 .../{AdhocFilter.test.js => AdhocFilter.test.ts}   |   0
 .../AdhocFilter/{index.js => index.ts}             | 110 ++-
 .../AdhocFilterControl/AdhocFilterControl.test.tsx |  49 +-
 .../AdhocFilterControl/{index.jsx => index.tsx}    | 160 +++--
 ...er.test.jsx => AdhocFilterEditPopover.test.tsx} |  24 +-
 .../{index.jsx => index.tsx}                       | 159 +++--
 ...AdhocFilterEditPopoverSimpleTabContent.test.tsx | 133 ++--
 .../index.tsx                                      |  88 ++-
 .../FilterControl/AdhocFilterOption/index.tsx      |  25 +-
 .../AdhocFilterPopoverTrigger/index.tsx            |   2 +-
 .../utils/useDatePickerInAdhocFilter.tsx           |   6 +-
 .../FilterControl/utils/useGetTimeRangeLabel.tsx   |  31 +-
 .../FixedOrMetricControl/{index.jsx => index.tsx}  |  88 ++-
 .../{AdhocMetric.test.js => AdhocMetric.test.ts}   |   4 +-
 .../{AdhocMetric.js => AdhocMetric.ts}             | 102 ++-
 .../{index.jsx => index.tsx}                       | 162 ++++-
 ...cOption.test.jsx => AdhocMetricOption.test.tsx} |  27 +-
 ...AdhocMetricOption.jsx => AdhocMetricOption.tsx} |  46 +-
 .../MetricControl/AdhocMetricPopoverTrigger.tsx    |  20 +-
 ...on.test.jsx => FilterDefinitionOption.test.tsx} |   6 +-
 ...nitionOption.jsx => FilterDefinitionOption.tsx} |  21 +-
 ...lue.test.jsx => MetricDefinitionValue.test.tsx} |  26 +-
 ...finitionValue.jsx => MetricDefinitionValue.tsx} |  31 +-
 ...icsControl.test.jsx => MetricsControl.test.tsx} |  17 +-
 .../{MetricsControl.jsx => MetricsControl.tsx}     |  93 ++-
 .../{adhocMetricType.js => adhocMetricType.ts}     |   0
 .../components/controls/MetricControl/types.ts     |   2 +
 .../components/controls/OptionControls/index.tsx   |   4 +-
 ...lectControl.test.jsx => SelectControl.test.tsx} |  34 +-
 .../{SelectControl.jsx => SelectControl.tsx}       | 152 ++--
 .../{SpatialControl.jsx => SpatialControl.tsx}     | 123 ++--
 ...eaControl.test.jsx => TextAreaControl.test.tsx} |   0
 .../{TextAreaControl.jsx => TextAreaControl.tsx}   | 104 ++-
 .../components/controls/TextControl/index.tsx      |   4 +
 .../{index.jsx => index.tsx}                       | 109 ++-
 ...rtControl.test.jsx => ViewportControl.test.tsx} |   0
 .../{ViewportControl.jsx => ViewportControl.tsx}   |  82 ++-
 .../components/controls/XAxisSortControl.tsx       |  16 +-
 .../explore/controlUtils/standardizedFormData.ts   |  24 +-
 .../src/explore/{controls.jsx => controls.tsx}     |  79 +-
 .../src/explore/{store.test.jsx => store.test.tsx} |   5 +-
 .../src/explore/{store.js => store.ts}             |  62 +-
 69 files changed, 3271 insertions(+), 1481 deletions(-)

diff --git a/superset-frontend/playwright/tests/experimental/README.md b/superset-frontend/playwright/tests/experimental/README.md
index a1511695b1..55a2399170 100644
--- a/superset-frontend/playwright/tests/experimental/README.md
+++ b/superset-frontend/playwright/tests/experimental/README.md
@@ -28,12 +28,14 @@ This directory contains **experimental** Playwright E2E tests that are being dev
 ### Running Tests
 
 **By default (CI and local), experimental tests are EXCLUDED:**
+
 ```bash
 npm run playwright:test
 # Only runs stable tests (tests/auth/*)
 ```
 
 **To include experimental tests, set the environment variable:**
+
 ```bash
 INCLUDE_EXPERIMENTAL=true npm run playwright:test
 # Runs all tests including experimental/
@@ -60,6 +62,7 @@ testIgnore: process.env.INCLUDE_EXPERIMENTAL
 ```
 
 This ensures:
+
 - Without `INCLUDE_EXPERIMENTAL`: Tests in `experimental/` are ignored
 - With `INCLUDE_EXPERIMENTAL=true`: All tests run, including experimental
 
@@ -77,11 +80,13 @@ Add tests to `experimental/` when:
 Once an experimental test has proven stable (consistent CI passes over time):
 
 1. **Move the test file** from `experimental/` to the appropriate stable directory:
+
    ```bash
    git mv tests/experimental/dataset/my-test.spec.ts tests/dataset/my-test.spec.ts
    ```
 
 2. **Commit the move** with a clear message:
+
    ```bash
    git commit -m "test(playwright): promote my-test from experimental to stable"
    ```
@@ -102,11 +107,13 @@ Once an experimental test has proven stable (consistent CI passes over time):
 **Important**: Supporting infrastructure (components, page objects, API helpers) should live in **stable locations**, NOT under `experimental/`:
 
 ✅ **Correct locations:**
+
 - `playwright/components/` - Components used by any tests
 - `playwright/pages/` - Page objects for any features
 - `playwright/helpers/api/` - API helpers for test data setup
 
 ❌ **Avoid:**
+
 - `playwright/tests/experimental/components/` - Makes it hard to share infrastructure
 
 This keeps infrastructure reusable and avoids duplication when tests graduate from experimental to stable.
diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.ts
similarity index 60%
rename from superset-frontend/src/SqlLab/actions/sqlLab.js
rename to superset-frontend/src/SqlLab/actions/sqlLab.ts
index a4aead7e12..5d9bb391cb 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.js
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts
@@ -18,6 +18,8 @@
  */
 import { nanoid } from 'nanoid';
 import rison from 'rison';
+import type { ThunkAction } from 'redux-thunk';
+import type { QueryColumn, SupersetError } from '@superset-ui/core';
 import {
   FeatureFlag,
   SupersetClient,
@@ -38,10 +40,76 @@ import {
 import { LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY } from 'src/logger/LogUtils';
 import getBootstrapData from 'src/utils/getBootstrapData';
 import { logEvent } from 'src/logger/actions';
+import type { QueryEditor, SqlLabRootState, Table } from '../types';
 import { newQueryTabName } from '../utils/newQueryTabName';
 import getInitialState from '../reducers/getInitialState';
 import { rehydratePersistedState } from '../utils/reduxStateToLocalStorageHelper';
 
+// Type definitions for SqlLab actions
+export interface Query {
+  id: string;
+  dbId?: number;
+  sql: string;
+  sqlEditorId?: string | null;
+  sqlEditorImmutableId?: string;
+  tab?: string | null;
+  catalog?: string | null;
+  schema?: string | null;
+  tempTable?: string | null;
+  templateParams?: string;
+  queryLimit?: number;
+  runAsync?: boolean;
+  ctas?: boolean;
+  ctas_method?: string;
+  isDataPreview?: boolean;
+  progress?: number;
+  startDttm?: number;
+  endDttm?: number;
+  state?: string;
+  cached?: boolean;
+  resultsKey?: string | null;
+  updateTabState?: boolean;
+  tableName?: string;
+  link?: string;
+  inLocalStorage?: boolean;
+  executedSql?: string;
+  query_id?: number;
+}
+
+export interface Database {
+  id: number;
+  allow_run_async: boolean;
+  disable_data_preview?: boolean;
+}
+
+/**
+ * Query result data from the SQL execute API (matches QueryResultSchema)
+ */
+export interface SqlExecuteQueryResult {
+  endDttm?: number;
+  executedSql?: string;
+  limit?: number;
+  limitingFactor?: string;
+  tempTable?: string | null;
+  progress?: number;
+  state?: string;
+  [key: string]: unknown;
+}
+
+/**
+ * Response from /api/v1/sqllab/execute/
+ * This matches QueryExecutionResponseSchema from the backend
+ */
+export interface SqlExecuteResponse {
+  status?: string;
+  data: Record<string, unknown>[];
+  columns: QueryColumn[];
+  selected_columns?: QueryColumn[];
+  expanded_columns?: QueryColumn[];
+  query: SqlExecuteQueryResult;
+  query_id?: number;
+}
+
 export const RESET_STATE = 'RESET_STATE';
 export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
 export const UPDATE_QUERY_EDITOR = 'UPDATE_QUERY_EDITOR';
@@ -104,6 +172,71 @@ export const SET_EDITOR_TAB_LAST_UPDATE = 'SET_EDITOR_TAB_LAST_UPDATE';
 export const SET_LAST_UPDATED_ACTIVE_TAB = 'SET_LAST_UPDATED_ACTIVE_TAB';
 export const CLEAR_DESTROYED_QUERY_EDITOR = 'CLEAR_DESTROYED_QUERY_EDITOR';
 
+// SqlLab action interface with optional properties for reducer compatibility.
+// Uses explicit property types instead of `any` for better type safety.
+export interface SqlLabAction {
+  type: string;
+  // Query editor related
+  queryEditor?: QueryEditor | Partial<QueryEditor>;
+  alterations?: Partial<QueryEditor>;
+  oldQueryEditor?: QueryEditor;
+  newQueryEditor?: QueryEditor;
+  queryEditorId?: string;
+  // Query related
+  query?: Query | Partial<Query>;
+  queries?: Query[];
+  alteredQueries?: Record<string, Partial<Query>>;
+  queryId?: string;
+  // Table related
+  table?: Table | Partial<Table>;
+  tables?: Table[];
+  oldTable?: Table;
+  newTable?: Table;
+  // Database related
+  databases?: Database[] | Record<string, Database>;
+  dbId?: number;
+  // State and config
+  sqlLabInitialState?: SqlLabRootState['sqlLab'];
+  sql?: string | null;
+  name?: string;
+  catalog?: string | null;
+  schema?: string | null;
+  autorun?: boolean;
+  templateParams?: string;
+  queryLimit?: number;
+  position?: { row: number; column: number };
+  functionNames?: string[];
+  northPercent?: number;
+  southPercent?: number;
+  hideLeftBar?: boolean;
+  // Results and errors
+  results?: SqlExecuteResponse;
+  errors?: SupersetError[];
+  error?: string;
+  msg?: string;
+  link?: string;
+  err?: string;
+  // Misc
+  tabId?: string | number;
+  timestamp?: number;
+  interval?: number;
+  offline?: boolean;
+  datasource?: unknown;
+  clientId?: string;
+  result?: { remoteId: number };
+  prepend?: boolean;
+  json?: { result: unknown };
+  oldQueryId?: string;
+  newQuery?: { id: string };
+}
+
+type SqlLabThunkAction<R = void> = ThunkAction<
+  R,
+  SqlLabRootState,
+  unknown,
+  SqlLabAction
+>;
+
 export const addInfoToast = addInfoToastAction;
 export const addSuccessToast = addSuccessToastAction;
 export const addDangerToast = addDangerToastAction;
@@ -112,13 +245,14 @@ export const addWarningToast = addWarningToastAction;
 export const CtasEnum = {
   Table: 'TABLE',
   View: 'VIEW',
-};
+} as const;
+
 const ERR_MSG_CANT_LOAD_QUERY = t("The query couldn't be loaded");
 
 // a map of SavedQuery field names to the different names used client-side,
 // because for now making the names consistent is too complicated
 // so it might as well only happen in one place
-const queryClientMapping = {
+const queryClientMapping: Record<string, string> = {
   id: 'remoteId',
   db_id: 'dbId',
   label: 'name',
@@ -127,26 +261,36 @@ const queryClientMapping = {
 const queryServerMapping = invert(queryClientMapping);
 
 // uses a mapping like those above to convert object key names to another style
-const fieldConverter = mapping => obj =>
-  mapKeys(obj, (value, key) => (key in mapping ? mapping[key] : key));
+const fieldConverter =
+  (mapping: Record<string, string>) =>
+  <T extends Record<string, unknown>>(obj: T): Record<string, unknown> =>
+    mapKeys(obj, (_value, key) => (key in mapping ? mapping[key] : key));
 
 export const convertQueryToServer = fieldConverter(queryServerMapping);
 export const convertQueryToClient = fieldConverter(queryClientMapping);
 
-export function getUpToDateQuery(rootState, queryEditor, key) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function getUpToDateQuery(
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  rootState: any,
+  queryEditor: Partial<QueryEditor>,
+  key?: string,
+): QueryEditor {
   const {
     sqlLab: { unsavedQueryEditor, queryEditors },
   } = rootState;
   const id = key ?? queryEditor.id;
   return {
     id,
-    ...queryEditors.find(qe => qe.id === id),
+    ...queryEditors.find((qe: QueryEditor) => qe.id === id),
     ...(id === unsavedQueryEditor.id && unsavedQueryEditor),
-  };
+  } as QueryEditor;
 }
 
-export function resetState(data) {
-  return (dispatch, getState) => {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function resetState(data?: Record<string, unknown>): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any, getState: any) => {
     const { common } = getState();
     const initialState = getInitialState({
       ...getBootstrapData(),
@@ -158,20 +302,25 @@ export function resetState(data) {
       type: RESET_STATE,
       sqlLabInitialState: initialState.sqlLab,
     });
-    rehydratePersistedState(dispatch, initialState);
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    rehydratePersistedState(dispatch, initialState as any);
   };
 }
 
-export function updateQueryEditor(alterations) {
+export function updateQueryEditor(
+  alterations: Partial<QueryEditor>,
+): SqlLabAction {
   return { type: UPDATE_QUERY_EDITOR, alterations };
 }
 
-export function setEditorTabLastUpdate(timestamp) {
+export function setEditorTabLastUpdate(timestamp: number): SqlLabAction {
   return { type: SET_EDITOR_TAB_LAST_UPDATE, timestamp };
 }
 
-export function scheduleQuery(query) {
-  return dispatch =>
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function scheduleQuery(query: Record<string, unknown>): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any) =>
     SupersetClient.post({
       endpoint: '/api/v1/saved_query/',
       jsonPayload: query,
@@ -191,8 +340,10 @@ export function scheduleQuery(query) {
       );
 }
 
-export function estimateQueryCost(queryEditor) {
-  return (dispatch, getState) => {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function estimateQueryCost(queryEditor: QueryEditor): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any, getState: any) => {
     const { dbId, catalog, schema, sql, selectedText, templateParams } =
       getUpToDateQuery(getState(), queryEditor);
     const requestSql = selectedText || sql;
@@ -232,11 +383,11 @@ export function estimateQueryCost(queryEditor) {
   };
 }
 
-export function clearInactiveQueries(interval) {
+export function clearInactiveQueries(interval: number): SqlLabAction {
   return { type: CLEAR_INACTIVE_QUERIES, interval };
 }
 
-export function startQuery(query, runPreviewOnly) {
+export function startQuery(query: Query, runPreviewOnly?: boolean) {
   Object.assign(query, {
     id: query.id ? query.id : nanoid(11),
     progress: 0,
@@ -244,14 +395,17 @@ export function startQuery(query, runPreviewOnly) {
     state: query.runAsync ? 'pending' : 'running',
     cached: false,
   });
-  return { type: START_QUERY, query, runPreviewOnly };
+  return { type: START_QUERY, query, runPreviewOnly } as const;
 }
 
-export function querySuccess(query, results) {
-  return { type: QUERY_SUCCESS, query, results };
+export function querySuccess(query: Query, results: SqlExecuteResponse) {
+  return { type: QUERY_SUCCESS, query, results } as const;
 }
 
-export function logFailedQuery(query, errors) {
+export function logFailedQuery(
+  query: Query,
+  errors?: SupersetError[],
+): SqlLabThunkAction<void> {
   return function (dispatch) {
     const eventData = {
       has_err: true,
@@ -259,7 +413,9 @@ export function logFailedQuery(query, errors) {
       ts: new Date().getTime(),
     };
     errors?.forEach(({ error_type: errorType, message, extra }) => {
-      const issueCodes = extra?.issue_codes?.map(({ code }) => code) || [-1];
+      const issueCodes = (
+        extra as { issue_codes?: { code: number }[] }
+      )?.issue_codes?.map(({ code }) => code) || [-1];
       dispatch(
         logEvent(LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY, {
           ...eventData,
@@ -272,35 +428,51 @@ export function logFailedQuery(query, errors) {
   };
 }
 
-export function createQueryFailedAction(query, msg, link, errors) {
-  return { type: QUERY_FAILED, query, msg, link, errors };
+export function createQueryFailedAction(
+  query: Query,
+  msg: string,
+  link?: string,
+  errors?: SupersetError[],
+) {
+  return { type: QUERY_FAILED, query, msg, link, errors } as const;
 }
 
-export function queryFailed(query, msg, link, errors) {
+export function queryFailed(
+  query: Query,
+  msg: string,
+  link?: string,
+  errors?: SupersetError[],
+): SqlLabThunkAction<void> {
   return function (dispatch) {
     dispatch(logFailedQuery(query, errors));
     dispatch(createQueryFailedAction(query, msg, link, errors));
   };
 }
 
-export function stopQuery(query) {
-  return { type: STOP_QUERY, query };
+export function stopQuery(query: Query) {
+  return { type: STOP_QUERY, query } as const;
 }
 
-export function clearQueryResults(query) {
+export function clearQueryResults(query: Query): SqlLabAction {
   return { type: CLEAR_QUERY_RESULTS, query };
 }
 
-export function removeDataPreview(table) {
+export function removeDataPreview(table: Table): SqlLabAction {
   return { type: REMOVE_DATA_PREVIEW, table };
 }
 
-export function requestQueryResults(query) {
+export function requestQueryResults(query: Query): SqlLabAction {
   return { type: REQUEST_QUERY_RESULTS, query };
 }
 
-export function fetchQueryResults(query, displayLimit, timeoutInMs) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function fetchQueryResults(
+  query: Query,
+  displayLimit?: number,
+  timeoutInMs?: number,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const { SQLLAB_QUERY_RESULT_TIMEOUT } = getState().common?.conf ?? {};
     dispatch(requestQueryResults(query));
 
@@ -315,7 +487,9 @@ export function fetchQueryResults(query, displayLimit, timeoutInMs) {
       parseMethod: 'json-bigint',
       ...(timeout && { timeout, signal: controller.signal }),
     })
-      .then(({ json }) => dispatch(querySuccess(query, json)))
+      .then(({ json }) =>
+        dispatch(querySuccess(query, json as SqlExecuteResponse)),
+      )
       .catch(response => {
         controller.abort();
         getClientErrorObject(response).then(error => {
@@ -332,8 +506,10 @@ export function fetchQueryResults(query, displayLimit, timeoutInMs) {
   };
 }
 
-export function runQuery(query, runPreviewOnly) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function runQuery(query: Query, runPreviewOnly?: boolean): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     dispatch(startQuery(query, runPreviewOnly));
     const postPayload = {
       client_id: query.id,
@@ -361,7 +537,7 @@ export function runQuery(query, runPreviewOnly) {
     })
       .then(({ json }) => {
         if (!query.runAsync) {
-          dispatch(querySuccess(query, json));
+          dispatch(querySuccess(query, json as SqlExecuteResponse));
         }
       })
       .catch(response =>
@@ -380,17 +556,20 @@ export function runQuery(query, runPreviewOnly) {
   };
 }
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function runQueryFromSqlEditor(
-  database,
-  queryEditor,
-  defaultQueryLimit,
-  tempTable,
-  ctas,
-  ctasMethod,
-) {
-  return function (dispatch, getState) {
+  database: Database | null,
+  queryEditor: QueryEditor,
+  defaultQueryLimit: number,
+  tempTable?: string,
+  ctas?: boolean,
+  ctasMethod?: string,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
-    const query = {
+    const query: Query = {
+      id: nanoid(11),
       dbId: qe.dbId,
       sql: qe.selectedText || qe.sql,
       sqlEditorId: qe.tabViewId ?? qe.id,
@@ -410,15 +589,19 @@ export function runQueryFromSqlEditor(
   };
 }
 
-export function reRunQuery(query) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function reRunQuery(query: Query): any {
   // run Query with a new id
-  return function (dispatch) {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     dispatch(runQuery({ ...query, id: nanoid(11) }));
   };
 }
 
-export function postStopQuery(query) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function postStopQuery(query: Query): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     return SupersetClient.post({
       endpoint: '/api/v1/query/stop',
       body: JSON.stringify({ client_id: query.id }),
@@ -430,11 +613,17 @@ export function postStopQuery(query) {
   };
 }
 
-export function setDatabases(databases) {
+export function setDatabases(databases: Database[]): SqlLabAction {
   return { type: SET_DATABASES, databases };
 }
 
-function migrateTable(table, queryEditorId, dispatch) {
+function migrateTable(
+  table: Table,
+  queryEditorId: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  dispatch: any,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Promise<any> {
   return SupersetClient.post({
     endpoint: encodeURI('/tableschemaview/'),
     postPayload: { table: { ...table, queryEditorId } },
@@ -459,7 +648,13 @@ function migrateTable(table, queryEditorId, dispatch) {
     );
 }
 
-function migrateQuery(queryId, queryEditorId, dispatch) {
+function migrateQuery(
+  queryId: string,
+  queryEditorId: string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  dispatch: any,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Promise<any> {
   return SupersetClient.post({
     endpoint: encodeURI(`/tabstateview/${queryEditorId}/migrate_query`),
     postPayload: { queryId },
@@ -486,14 +681,18 @@ function migrateQuery(queryId, queryEditorId, dispatch) {
  * stored in local storage will also be synchronized to the backend
  * through syncQueryEditor.
  */
-export function syncQueryEditor(queryEditor) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function syncQueryEditor(queryEditor: QueryEditor): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const { tables, queries } = getState().sqlLab;
     const localStorageTables = tables.filter(
-      table => table.inLocalStorage && table.queryEditorId === queryEditor.id,
+      (table: Table) =>
+        table.inLocalStorage && table.queryEditorId === queryEditor.id,
     );
     const localStorageQueries = Object.values(queries).filter(
-      query => query.inLocalStorage && query.sqlEditorId === queryEditor.id,
+      (query: Query) =>
+        query.inLocalStorage && query.sqlEditorId === queryEditor.id,
     );
     return SupersetClient.post({
       endpoint: '/tabstateview/',
@@ -512,11 +711,11 @@ export function syncQueryEditor(queryEditor) {
           newQueryEditor,
         });
         return Promise.all([
-          ...localStorageTables.map(table =>
-            migrateTable(table, newQueryEditor.tabViewId, dispatch),
+          ...localStorageTables.map((table: Table) =>
+            migrateTable(table, newQueryEditor.tabViewId!, dispatch),
           ),
-          ...localStorageQueries.map(query =>
-            migrateQuery(query.id, newQueryEditor.tabViewId, dispatch),
+          ...localStorageQueries.map((query: Query) =>
+            migrateQuery(query.id, newQueryEditor.tabViewId!, dispatch),
           ),
         ]);
       })
@@ -533,7 +732,9 @@ export function syncQueryEditor(queryEditor) {
   };
 }
 
-export function addQueryEditor(queryEditor) {
+export function addQueryEditor(
+  queryEditor: Partial<QueryEditor>,
+): SqlLabAction {
   const newQueryEditor = {
     ...queryEditor,
     id: nanoid(11),
@@ -547,24 +748,28 @@ export function addQueryEditor(queryEditor) {
   };
 }
 
-export function addNewQueryEditor() {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function addNewQueryEditor(): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const {
       sqlLab: { queryEditors, tabHistory, unsavedQueryEditor, databases },
       common,
     } = getState();
     const defaultDbId = common.conf.SQLLAB_DEFAULT_DBID;
     const activeQueryEditor = queryEditors.find(
-      qe => qe.id === tabHistory[tabHistory.length - 1],
+      (qe: QueryEditor) => qe.id === tabHistory[tabHistory.length - 1],
+    );
+    const dbIds = Object.values(databases).map(
+      (database: Database) => database.id,
     );
-    const dbIds = Object.values(databases).map(database => database.id);
     const firstDbId = dbIds.length > 0 ? Math.min(...dbIds) : undefined;
     const { dbId, catalog, schema, queryLimit, autorun } = {
       ...queryEditors[0],
       ...activeQueryEditor,
       ...(unsavedQueryEditor.id === activeQueryEditor?.id &&
         unsavedQueryEditor),
-    };
+    } as Partial<QueryEditor>;
     const warning = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
       ? ''
       : t(
@@ -572,7 +777,7 @@ export function addNewQueryEditor() {
         );
 
     const name = newQueryTabName(
-      queryEditors?.map(qe => ({
+      queryEditors?.map((qe: QueryEditor) => ({
         ...qe,
         ...(qe.id === unsavedQueryEditor.id && unsavedQueryEditor),
       })) || [],
@@ -581,8 +786,8 @@ export function addNewQueryEditor() {
     return dispatch(
       addQueryEditor({
         dbId: dbId || defaultDbId || firstDbId,
-        catalog: catalog ?? null,
-        schema: schema ?? null,
+        catalog,
+        schema,
         autorun: autorun ?? false,
         sql: `${warning}SELECT ...`,
         queryLimit: queryLimit || common.conf.DEFAULT_SQLLAB_LIMIT,
@@ -592,20 +797,25 @@ export function addNewQueryEditor() {
   };
 }
 
-export function cloneQueryToNewTab(query, autorun) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function cloneQueryToNewTab(query: Query, autorun: boolean): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const state = getState();
     const { queryEditors, unsavedQueryEditor, tabHistory } = state.sqlLab;
     const sourceQueryEditor = {
-      ...queryEditors.find(qe => qe.id === tabHistory[tabHistory.length - 1]),
+      ...queryEditors.find(
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        (qe: any) => qe.id === tabHistory[tabHistory.length - 1],
+      ),
       ...(tabHistory[tabHistory.length - 1] === unsavedQueryEditor.id &&
         unsavedQueryEditor),
     };
     const queryEditor = {
       name: t('Copy of %s', sourceQueryEditor.name),
-      dbId: query.dbId ? query.dbId : null,
-      catalog: query.catalog ? query.catalog : null,
-      schema: query.schema ? query.schema : null,
+      dbId: query.dbId,
+      catalog: query.catalog,
+      schema: query.schema ?? undefined,
       autorun,
       sql: query.sql,
       queryLimit: sourceQueryEditor.queryLimit,
@@ -616,26 +826,31 @@ export function cloneQueryToNewTab(query, autorun) {
   };
 }
 
-export function setLastUpdatedActiveTab(queryEditorId) {
+export function setLastUpdatedActiveTab(queryEditorId: string): SqlLabAction {
   return {
     type: SET_LAST_UPDATED_ACTIVE_TAB,
     queryEditorId,
   };
 }
 
-export function setActiveQueryEditor(queryEditor) {
+export function setActiveQueryEditor(queryEditor: QueryEditor): SqlLabAction {
   return {
     type: SET_ACTIVE_QUERY_EDITOR,
     queryEditor,
   };
 }
 
-export function switchQueryEditor(goBackward = false) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function switchQueryEditor(goBackward = false): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const { sqlLab } = getState();
     const { queryEditors, tabHistory } = sqlLab;
     const qeid = tabHistory[tabHistory.length - 1];
-    const currentIndex = queryEditors.findIndex(qe => qe.id === qeid);
+    const currentIndex = queryEditors.findIndex(
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      (qe: any) => qe.id === qeid,
+    );
     const nextIndex = goBackward
       ? currentIndex - 1 + queryEditors.length
       : currentIndex + 1;
@@ -645,14 +860,32 @@ export function switchQueryEditor(goBackward = false) {
   };
 }
 
-export function loadQueryEditor(queryEditor) {
+export function loadQueryEditor(queryEditor: QueryEditor): SqlLabAction {
   return { type: LOAD_QUERY_EDITOR, queryEditor };
 }
 
-export function setTables(tableSchemas) {
+interface TableSchema {
+  description: {
+    columns: unknown[];
+    selectStar: string;
+    primaryKey: unknown;
+    foreignKeys: unknown[];
+    indexes: unknown[];
+    dataPreviewQueryId: string;
+  } | null;
+  database_id: number;
+  tab_state_id: number;
+  catalog: string;
+  schema: string;
+  table: string;
+  expanded: boolean;
+  id: string;
+}
+
+export function setTables(tableSchemas: TableSchema[]): SqlLabAction {
   const tables = tableSchemas
-    .filter(tableSchema => tableSchema.description !== null)
-    .map(tableSchema => {
+    .filter((tableSchema: TableSchema) => tableSchema.description !== null)
+    .map((tableSchema: TableSchema) => {
       const {
         columns,
         selectStar,
@@ -660,7 +893,7 @@ export function setTables(tableSchemas) {
         foreignKeys,
         indexes,
         dataPreviewQueryId,
-      } = tableSchema.description;
+      } = tableSchema.description!;
       return {
         dbId: tableSchema.database_id,
         queryEditorId: tableSchema.tab_state_id.toString(),
@@ -682,8 +915,13 @@ export function setTables(tableSchemas) {
   return { type: SET_TABLES, tables };
 }
 
-export function fetchQueryEditor(queryEditor, displayLimit) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function fetchQueryEditor(
+  queryEditor: QueryEditor,
+  displayLimit: number,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
     SupersetClient.get({
       endpoint: encodeURI(`/tabstateview/${queryEditorId}`),
@@ -705,9 +943,9 @@ export function fetchQueryEditor(queryEditor, displayLimit) {
           remoteId: json.saved_query?.id,
           hideLeftBar: json.hide_left_bar,
         };
-        dispatch(loadQueryEditor(loadedQueryEditor));
+        dispatch(loadQueryEditor(loadedQueryEditor as unknown as QueryEditor));
         dispatch(setTables(json.table_schemas || []));
-        if (json.latest_query && json.latest_query.resultsKey) {
+        if (json.latest_query?.resultsKey) {
           dispatch(fetchQueryResults(json.latest_query, displayLimit));
         }
       })
@@ -722,11 +960,11 @@ export function fetchQueryEditor(queryEditor, displayLimit) {
   };
 }
 
-export function setActiveSouthPaneTab(tabId) {
+export function setActiveSouthPaneTab(tabId: string): SqlLabAction {
   return { type: SET_ACTIVE_SOUTHPANE_TAB, tabId };
 }
 
-export function toggleLeftBar(queryEditor) {
+export function toggleLeftBar(queryEditor: QueryEditor): SqlLabAction {
   const hideLeftBar = !queryEditor.hideLeftBar;
   return {
     type: QUERY_EDITOR_TOGGLE_LEFT_BAR,
@@ -735,18 +973,21 @@ export function toggleLeftBar(queryEditor) {
   };
 }
 
-export function clearDestoryedQueryEditor(queryEditorId) {
+export function clearDestoryedQueryEditor(queryEditorId: string): SqlLabAction {
   return { type: CLEAR_DESTROYED_QUERY_EDITOR, queryEditorId };
 }
 
-export function removeQueryEditor(queryEditor) {
+export function removeQueryEditor(queryEditor: QueryEditor): SqlLabAction {
   return { type: REMOVE_QUERY_EDITOR, queryEditor };
 }
 
-export function removeAllOtherQueryEditors(queryEditor) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function removeAllOtherQueryEditors(queryEditor: QueryEditor): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const { sqlLab } = getState();
-    sqlLab.queryEditors?.forEach(otherQueryEditor => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    sqlLab.queryEditors?.forEach((otherQueryEditor: any) => {
       if (otherQueryEditor.id !== queryEditor.id) {
         dispatch(removeQueryEditor(otherQueryEditor));
       }
@@ -754,8 +995,10 @@ export function removeAllOtherQueryEditors(queryEditor) {
   };
 }
 
-export function removeQuery(query) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function removeQuery(query: Query): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const queryEditorId = query.sqlEditorId ?? query.id;
     const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
       ? SupersetClient.delete({
@@ -779,11 +1022,17 @@ export function removeQuery(query) {
   };
 }
 
-export function queryEditorSetDb(queryEditor, dbId) {
+export function queryEditorSetDb(
+  queryEditor: Partial<QueryEditor>,
+  dbId: number,
+): SqlLabAction {
   return { type: QUERY_EDITOR_SETDB, queryEditor, dbId };
 }
 
-export function queryEditorSetCatalog(queryEditor, catalog) {
+export function queryEditorSetCatalog(
+  queryEditor: Partial<QueryEditor> | null,
+  catalog: string | null,
+): SqlLabAction {
   return {
     type: QUERY_EDITOR_SET_CATALOG,
     queryEditor: queryEditor || {},
@@ -791,7 +1040,10 @@ export function queryEditorSetCatalog(queryEditor, catalog) {
   };
 }
 
-export function queryEditorSetSchema(queryEditor, schema) {
+export function queryEditorSetSchema(
+  queryEditor: Partial<QueryEditor> | null,
+  schema: string | null,
+): SqlLabAction {
   return {
     type: QUERY_EDITOR_SET_SCHEMA,
     queryEditor: queryEditor || {},
@@ -799,11 +1051,18 @@ export function queryEditorSetSchema(queryEditor, schema) {
   };
 }
 
-export function queryEditorSetAutorun(queryEditor, autorun) {
+export function queryEditorSetAutorun(
+  queryEditor: Partial<QueryEditor>,
+  autorun: boolean,
+): SqlLabAction {
   return { type: QUERY_EDITOR_SET_AUTORUN, queryEditor, autorun };
 }
 
-export function queryEditorSetTitle(queryEditor, name, id) {
+export function queryEditorSetTitle(
+  queryEditor: Partial<QueryEditor>,
+  name: string,
+  id: string,
+): SqlLabAction {
   return {
     type: QUERY_EDITOR_SET_TITLE,
     queryEditor: { ...queryEditor, id },
@@ -811,13 +1070,19 @@ export function queryEditorSetTitle(queryEditor, name, id) {
   };
 }
 
-export function saveQuery(query, clientId) {
-  const { id, ...payload } = convertQueryToServer(query);
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function saveQuery(query: Partial<QueryEditor>, clientId: string): any {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const { id: _id, ...payload } = convertQueryToServer(
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    query as any,
+  );
 
-  return dispatch =>
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any) =>
     SupersetClient.post({
       endpoint: '/api/v1/saved_query/',
-      jsonPayload: convertQueryToServer(payload),
+      jsonPayload: convertQueryToServer(payload as Record<string, unknown>),
     })
       .then(result => {
         const savedQuery = convertQueryToClient({
@@ -830,7 +1095,7 @@ export function saveQuery(query, clientId) {
           clientId,
           result: savedQuery,
         });
-        dispatch(queryEditorSetTitle(query, query.name, clientId));
+        dispatch(queryEditorSetTitle(query, query.name ?? '', clientId));
         return savedQuery;
       })
       .catch(() =>
@@ -838,36 +1103,49 @@ export function saveQuery(query, clientId) {
       );
 }
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const addSavedQueryToTabState =
-  (queryEditor, savedQuery) => dispatch => {
-    const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
-    const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
-      ? SupersetClient.put({
-          endpoint: `/tabstateview/${queryEditorId}`,
-          postPayload: { saved_query_id: savedQuery.remoteId },
-        })
-      : Promise.resolve();
-
-    return sync
-      .catch(() => {
-        dispatch(addDangerToast(t('Your query was not properly saved')));
-      })
-      .then(() => {
-        dispatch(addSuccessToast(t('Your query was saved')));
-      });
-  };
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  (queryEditor: QueryEditor, savedQuery: { remoteId: string }): any =>
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    (dispatch: any) => {
+      const queryEditorId = queryEditor.tabViewId ?? queryEditor.id;
+      const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
+        ? SupersetClient.put({
+            endpoint: `/tabstateview/${queryEditorId}`,
+            postPayload: { saved_query_id: savedQuery.remoteId },
+          })
+        : Promise.resolve();
 
-export function updateSavedQuery(query, clientId) {
-  const { id, ...payload } = convertQueryToServer(query);
+      return sync
+        .catch(() => {
+          dispatch(addDangerToast(t('Your query was not properly saved')));
+        })
+        .then(() => {
+          dispatch(addSuccessToast(t('Your query was saved')));
+        });
+    };
 
-  return dispatch =>
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function updateSavedQuery(
+  query: Partial<QueryEditor>,
+  clientId: string,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const { id: _id, ...payload } = convertQueryToServer(
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    query as any,
+  );
+
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any) =>
     SupersetClient.put({
       endpoint: `/api/v1/saved_query/${query.remoteId}`,
-      jsonPayload: convertQueryToServer(payload),
+      jsonPayload: convertQueryToServer(payload as Record<string, unknown>),
     })
       .then(() => {
         dispatch(addSuccessToast(t('Your query was updated')));
-        dispatch(queryEditorSetTitle(query, query.name, clientId));
+        dispatch(queryEditorSetTitle(query, query.name ?? '', clientId));
       })
       .catch(e => {
         const message = t('Your query could not be updated');
@@ -878,16 +1156,29 @@ export function updateSavedQuery(query, clientId) {
       .then(() => dispatch(updateQueryEditor(query)));
 }
 
-export function queryEditorSetSql(queryEditor, sql, queryId) {
+export function queryEditorSetSql(
+  queryEditor: Partial<QueryEditor>,
+  sql: string,
+  queryId?: string,
+): SqlLabAction {
   return { type: QUERY_EDITOR_SET_SQL, queryEditor, sql, queryId };
 }
 
-export function queryEditorSetCursorPosition(queryEditor, position) {
+export function queryEditorSetCursorPosition(
+  queryEditor: Partial<QueryEditor>,
+  position: { row: number; column: number },
+): SqlLabAction {
   return { type: QUERY_EDITOR_SET_CURSOR_POSITION, queryEditor, position };
 }
 
-export function queryEditorSetAndSaveSql(targetQueryEditor, sql, queryId) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function queryEditorSetAndSaveSql(
+  targetQueryEditor: Partial<QueryEditor>,
+  sql: string,
+  queryId?: string,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const queryEditor = getUpToDateQuery(getState(), targetQueryEditor);
     // saved query and set tab state use this action
     dispatch(queryEditorSetSql(queryEditor, sql, queryId));
@@ -912,13 +1203,19 @@ export function queryEditorSetAndSaveSql(targetQueryEditor, sql, queryId) {
   };
 }
 
-export function formatQuery(queryEditor) {
-  return function (dispatch, getState) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function formatQuery(queryEditor: QueryEditor): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const { sql, dbId, templateParams } = getUpToDateQuery(
       getState(),
       queryEditor,
     );
-    const body = { sql };
+    const body: {
+      sql: string;
+      database_id?: number;
+      template_params?: string;
+    } = { sql };
 
     // Include database_id and template_params if available for Jinja processing
     if (dbId) {
@@ -942,7 +1239,10 @@ export function formatQuery(queryEditor) {
   };
 }
 
-export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
+export function queryEditorSetQueryLimit(
+  queryEditor: Partial<QueryEditor>,
+  queryLimit: number,
+): SqlLabAction {
   return {
     type: QUERY_EDITOR_SET_QUERY_LIMIT,
     queryEditor,
@@ -950,7 +1250,10 @@ export function queryEditorSetQueryLimit(queryEditor, queryLimit) {
   };
 }
 
-export function queryEditorSetTemplateParams(queryEditor, templateParams) {
+export function queryEditorSetTemplateParams(
+  queryEditor: Partial<QueryEditor>,
+  templateParams: string,
+): SqlLabAction {
   return {
     type: QUERY_EDITOR_SET_TEMPLATE_PARAMS,
     queryEditor,
@@ -958,22 +1261,31 @@ export function queryEditorSetTemplateParams(queryEditor, templateParams) {
   };
 }
 
-export function queryEditorSetSelectedText(queryEditor, sql) {
+export function queryEditorSetSelectedText(
+  queryEditor: Partial<QueryEditor>,
+  sql: string | null,
+): SqlLabAction {
   return { type: QUERY_EDITOR_SET_SELECTED_TEXT, queryEditor, sql };
 }
 
-export function mergeTable(table, query, prepend) {
+export function mergeTable(
+  table: Partial<Table>,
+  query?: Query,
+  prepend?: boolean,
+): SqlLabAction {
   return { type: MERGE_TABLE, table, query, prepend };
 }
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 export function addTable(
-  queryEditor,
-  tableName,
-  catalogName,
-  schemaName,
+  queryEditor: Partial<QueryEditor>,
+  tableName: string,
+  catalogName: string | null,
+  schemaName: string,
   expanded = true,
-) {
-  return function (dispatch, getState) {
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id);
     const table = {
       dbId,
@@ -992,8 +1304,24 @@ export function addTable(
   };
 }
 
-export function runTablePreviewQuery(newTable, runPreviewOnly) {
-  return function (dispatch, getState) {
+interface NewTable {
+  id?: string;
+  dbId: number | string;
+  catalog?: string | null;
+  schema?: string;
+  name: string;
+  queryEditorId?: string;
+  selectStar?: string;
+  previewQueryId?: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function runTablePreviewQuery(
+  newTable: NewTable,
+  runPreviewOnly?: boolean,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any, getState: any) {
     const {
       sqlLab: { databases },
     } = getState();
@@ -1001,12 +1329,12 @@ export function runTablePreviewQuery(newTable, runPreviewOnly) {
     const { dbId, catalog, schema } = newTable;
 
     if (database && !database.disable_data_preview) {
-      const dataPreviewQuery = {
+      const dataPreviewQuery: Query = {
         id: newTable.previewQueryId ?? nanoid(11),
-        dbId,
-        catalog,
-        schema,
-        sql: newTable.selectStar,
+        dbId: dbId as number | undefined,
+        catalog: catalog ?? undefined,
+        schema: schema ?? '',
+        sql: newTable.selectStar ?? '',
         tableName: newTable.name,
         sqlEditorId: null,
         tab: '',
@@ -1022,9 +1350,9 @@ export function runTablePreviewQuery(newTable, runPreviewOnly) {
           mergeTable(
             {
               id: newTable.id,
-              dbId: newTable.dbId,
-              catalog: newTable.catalog,
-              schema: newTable.schema,
+              dbId: newTable.dbId as number | undefined,
+              catalog: newTable.catalog ?? undefined,
+              schema: newTable.schema ?? '',
               name: newTable.name,
               queryEditorId: newTable.queryEditorId,
               dataPreviewQueryId: dataPreviewQuery.id,
@@ -1039,8 +1367,22 @@ export function runTablePreviewQuery(newTable, runPreviewOnly) {
   };
 }
 
-export function syncTable(table, tableMetadata, finalQueryEditorId) {
-  return function (dispatch) {
+interface TableMetaData {
+  columns?: unknown[];
+  selectStar?: string;
+  primaryKey?: unknown;
+  foreignKeys?: unknown[];
+  indexes?: unknown[];
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function syncTable(
+  table: Table,
+  tableMetadata: TableMetaData,
+  finalQueryEditorId?: string,
+): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const finalTable = { ...table, queryEditorId: finalQueryEditorId };
     const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
       ? SupersetClient.post({
@@ -1073,13 +1415,18 @@ export function syncTable(table, tableMetadata, finalQueryEditorId) {
   };
 }
 
-export function changeDataPreviewId(oldQueryId, newQuery) {
+export function changeDataPreviewId(
+  oldQueryId: string,
+  newQuery: Query,
+): SqlLabAction {
   return { type: CHANGE_DATA_PREVIEW_ID, oldQueryId, newQuery };
 }
 
-export function reFetchQueryResults(query) {
-  return function (dispatch) {
-    const newQuery = {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function reFetchQueryResults(query: Query): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
+    const newQuery: Query = {
       id: nanoid(),
       dbId: query.dbId,
       sql: query.sql,
@@ -1096,8 +1443,10 @@ export function reFetchQueryResults(query) {
   };
 }
 
-export function expandTable(table) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function expandTable(table: Table): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const sync =
       isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
       table.initialized
@@ -1122,8 +1471,10 @@ export function expandTable(table) {
   };
 }
 
-export function collapseTable(table) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function collapseTable(table: Table): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const sync =
       isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) &&
       table.initialized
@@ -1148,12 +1499,14 @@ export function collapseTable(table) {
   };
 }
 
-export function removeTables(tables) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function removeTables(tables: Table[]): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const tablesToRemove = tables?.filter(Boolean) ?? [];
     const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)
       ? Promise.all(
-          tablesToRemove.map(table =>
+          tablesToRemove.map((table: Table) =>
             table.initialized
               ? SupersetClient.delete({
                   endpoint: encodeURI(`/tableschemaview/${table.id}`),
@@ -1178,15 +1531,21 @@ export function removeTables(tables) {
   };
 }
 
-export function refreshQueries(alteredQueries) {
+export function refreshQueries(
+  alteredQueries: Record<string, Query>,
+): SqlLabAction {
   return { type: REFRESH_QUERIES, alteredQueries };
 }
 
-export function setUserOffline(offline) {
+export function setUserOffline(offline: boolean): SqlLabAction {
   return { type: SET_USER_OFFLINE, offline };
 }
 
-export function persistEditorHeight(queryEditor, northPercent, southPercent) {
+export function persistEditorHeight(
+  queryEditor: QueryEditor,
+  northPercent: number,
+  southPercent: number,
+): SqlLabAction {
   return {
     type: QUERY_EDITOR_PERSIST_HEIGHT,
     queryEditor,
@@ -1195,16 +1554,18 @@ export function persistEditorHeight(queryEditor, northPercent, southPercent) {
   };
 }
 
-export function popPermalink(key) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function popPermalink(key: string): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/${key}` })
       .then(({ json }) =>
         dispatch(
           addQueryEditor({
             name: json.name ? json.name : t('Shared query'),
-            dbId: json.dbId ? parseInt(json.dbId, 10) : null,
-            catalog: json.catalog ? json.catalog : null,
-            schema: json.schema ? json.schema : null,
+            dbId: json.dbId ? parseInt(json.dbId, 10) : undefined,
+            catalog: json.catalog ?? null,
+            schema: json.schema ?? undefined,
             autorun: json.autorun ? json.autorun : false,
             sql: json.sql ? json.sql : 'SELECT ...',
             templateParams: json.templateParams,
@@ -1215,8 +1576,10 @@ export function popPermalink(key) {
   };
 }
 
-export function popStoredQuery(urlId) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function popStoredQuery(urlId: string): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     return SupersetClient.get({
       endpoint: `/api/v1/sqllab/permalink/kv:${urlId}`,
     })
@@ -1224,9 +1587,9 @@ export function popStoredQuery(urlId) {
         dispatch(
           addQueryEditor({
             name: json.name ? json.name : t('Shared query'),
-            dbId: json.dbId ? parseInt(json.dbId, 10) : null,
-            catalog: json.catalog ? json.catalog : null,
-            schema: json.schema ? json.schema : null,
+            dbId: json.dbId ? parseInt(json.dbId, 10) : undefined,
+            catalog: json.catalog ?? null,
+            schema: json.schema ?? undefined,
             autorun: json.autorun ? json.autorun : false,
             sql: json.sql ? json.sql : 'SELECT ...',
             templateParams: json.templateParams,
@@ -1236,8 +1599,10 @@ export function popStoredQuery(urlId) {
       .catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
   };
 }
-export function popSavedQuery(saveQueryId) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function popSavedQuery(saveQueryId: string): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     return SupersetClient.get({
       endpoint: `/api/v1/saved_query/${saveQueryId}`,
     })
@@ -1246,23 +1611,25 @@ export function popSavedQuery(saveQueryId) {
           ...convertQueryToClient(json.result),
           loaded: true,
           autorun: false,
-        };
+        } as Record<string, unknown>;
         const tmpAdaptedProps = {
-          name: queryEditorProps.name,
-          dbId: queryEditorProps.database.id,
-          catalog: queryEditorProps.catalog,
-          schema: queryEditorProps.schema,
-          sql: queryEditorProps.sql,
-          templateParams: queryEditorProps.templateParams,
-          remoteId: queryEditorProps.remoteId,
+          name: queryEditorProps.name as string,
+          dbId: (queryEditorProps.database as { id: number }).id,
+          catalog: queryEditorProps.catalog as string,
+          schema: queryEditorProps.schema as string,
+          sql: queryEditorProps.sql as string,
+          templateParams: queryEditorProps.templateParams as string,
+          remoteId: queryEditorProps.remoteId as number | null,
         };
         return dispatch(addQueryEditor(tmpAdaptedProps));
       })
       .catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
   };
 }
-export function popQuery(queryId) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function popQuery(queryId: string): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     return SupersetClient.get({
       endpoint: `/api/v1/query/${queryId}`,
     })
@@ -1281,8 +1648,10 @@ export function popQuery(queryId) {
       .catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY)));
   };
 }
-export function popDatasourceQuery(datasourceKey, sql) {
-  return function (dispatch) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function popDatasourceQuery(datasourceKey: string, sql?: string): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return function (dispatch: any) {
     const QUERY_TEXT = t('Query');
     const datasetId = datasourceKey.split('__')[0];
 
@@ -1310,19 +1679,30 @@ export function popDatasourceQuery(datasourceKey, sql) {
       );
   };
 }
-export function createDatasourceStarted() {
+export function createDatasourceStarted(): SqlLabAction {
   return { type: CREATE_DATASOURCE_STARTED };
 }
-export function createDatasourceSuccess(data) {
+export function createDatasourceSuccess(data: { id: number }): SqlLabAction {
   const datasource = `${data.id}__table`;
   return { type: CREATE_DATASOURCE_SUCCESS, datasource };
 }
-export function createDatasourceFailed(err) {
+export function createDatasourceFailed(err: string): SqlLabAction {
   return { type: CREATE_DATASOURCE_FAILED, err };
 }
 
-export function createDatasource(vizOptions) {
-  return dispatch => {
+interface VizOptions {
+  dbId: number;
+  catalog?: string | null;
+  schema: string;
+  datasourceName: string;
+  sql: string;
+  templateParams?: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function createDatasource(vizOptions: VizOptions): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any) => {
     dispatch(createDatasourceStarted());
     const { dbId, catalog, schema, datasourceName, sql, templateParams } =
       vizOptions;
@@ -1341,7 +1721,7 @@ export function createDatasource(vizOptions) {
       }),
     })
       .then(({ json }) => {
-        dispatch(createDatasourceSuccess(json));
+        dispatch(createDatasourceSuccess(json as { id: number }));
 
         return Promise.resolve(json);
       })
@@ -1360,8 +1740,10 @@ export function createDatasource(vizOptions) {
   };
 }
 
-export function createCtasDatasource(vizOptions) {
-  return dispatch => {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function createCtasDatasource(vizOptions: Record<string, unknown>): any {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return (dispatch: any) => {
     dispatch(createDatasourceStarted());
     return SupersetClient.post({
       endpoint: '/api/v1/dataset/get_or_create/',
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
index e0f49b70e3..3e4a756467 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.test.ts
@@ -232,7 +232,8 @@ test('returns column keywords among selected tables', async () => {
     );
     storeWithSqlLab.dispatch(
       addTable(
-        { id: expectQueryEditorId },
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        { id: expectQueryEditorId } as any,
         expectTable,
         expectCatalog,
         expectSchema,
@@ -276,7 +277,8 @@ test('returns column keywords among selected tables', async () => {
   act(() => {
     storeWithSqlLab.dispatch(
       addTable(
-        { id: expectQueryEditorId },
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        { id: expectQueryEditorId } as any,
         unexpectedTable,
         expectCatalog,
         expectSchema,
diff --git a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
index 1a8eb7b368..4ab5a31964 100644
--- a/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
+++ b/superset-frontend/src/SqlLab/components/AceEditorWrapper/useKeywords.ts
@@ -149,10 +149,10 @@ export function useKeywords(
     if (data.meta === 'table') {
       dispatch(
         addTable(
-          { id: queryEditorId, dbId, tabViewId },
+          { id: String(queryEditorId), dbId: dbId as number, tabViewId },
           data.value,
-          catalog,
-          schema,
+          catalog ?? null,
+          schema ?? '',
           false, // Don't auto-expand/switch tabs when adding via autocomplete
         ),
       );
diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
index f941fb4587..2bf1aca284 100644
--- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
+++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
@@ -122,7 +122,7 @@ function QueryAutoRefresh({
                   dispatch(
                     queryFailed(
                       query,
-                      query.errorMessage,
+                      query.errorMessage ?? '',
                       query.extra?.errors?.[0]?.extra?.link,
                       query.extra?.errors,
                     ),
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index 17c5de8085..048486bcdb 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -344,9 +344,9 @@ export const SaveDatasetModal = ({
     dispatch(
       createDatasource({
         sql: datasource.sql,
-        dbId: datasource.dbId || datasource?.database?.id,
-        catalog: datasource?.catalog,
-        schema: datasource?.schema,
+        dbId: (datasource.dbId ?? datasource?.database?.id) as number,
+        catalog: datasource?.catalog ?? null,
+        schema: datasource?.schema ?? '',
         templateParams,
         datasourceName: datasetName,
       }),
diff --git a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
index b154371add..84256491ec 100644
--- a/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SouthPane/index.tsx
@@ -143,7 +143,9 @@ const SouthPane = ({
           ({ dbId, catalog, schema, name }) =>
             [dbId, catalog, schema, name].join(':') === key,
         );
-        dispatch(removeTables([table]));
+        if (table) {
+          dispatch(removeTables([table]));
+        }
       }
     },
     [dispatch, pinnedTables],
diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
index 9977778430..b3d3c75cfd 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.tsx
@@ -280,7 +280,8 @@ const SqlEditor: FC<Props> = ({
 
       dispatch(
         runQueryFromSqlEditor(
-          database,
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          database as any,
           queryEditor,
           defaultQueryLimit,
           ctasArg ? ctas : '',
@@ -565,8 +566,8 @@ const SqlEditor: FC<Props> = ({
   };
 
   const setQueryEditorAndSaveSql = useCallback(
-    sql => {
-      dispatch(queryEditorSetAndSaveSql(queryEditor, sql));
+    (sql: string) => {
+      dispatch(queryEditorSetAndSaveSql(queryEditor, sql, undefined));
     },
     [dispatch, queryEditor],
   );
@@ -578,7 +579,7 @@ const SqlEditor: FC<Props> = ({
 
   const onSqlChanged = useEffectEvent((sql: string) => {
     currentSQL.current = sql;
-    dispatch(queryEditorSetSql(queryEditor, sql));
+    dispatch(queryEditorSetSql(queryEditor, sql, undefined));
   });
 
   const getQueryCostEstimate = () => {
diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
index 5158ef7e3c..060ea2c6af 100644
--- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar/index.tsx
@@ -31,6 +31,7 @@ import {
   setDatabases,
   addDangerToast,
   resetState,
+  type Database,
 } from 'src/SqlLab/actions/sqlLab';
 import { Button, EmptyState, Icons } from '@superset-ui/core/components';
 import { type DatabaseObject } from 'src/components';
@@ -194,8 +195,8 @@ const SqlEditorLeftBar = ({ queryEditorId }: SqlEditorLeftBarProps) => {
   );
 
   const handleDbList = useCallback(
-    (result: DatabaseObject) => {
-      dispatch(setDatabases(result));
+    (result: DatabaseObject[]) => {
+      dispatch(setDatabases(result as unknown as Database[]));
     },
     [dispatch],
   );
diff --git a/superset-frontend/src/SqlLab/components/TableElement/index.tsx b/superset-frontend/src/SqlLab/components/TableElement/index.tsx
index 251c3cb7dc..0923db10f8 100644
--- a/superset-frontend/src/SqlLab/components/TableElement/index.tsx
+++ b/superset-frontend/src/SqlLab/components/TableElement/index.tsx
@@ -171,7 +171,7 @@ const TableElement = ({ table, ...props }: TableElementProps) => {
     dispatch(
       tableApiUtil.invalidateTags([{ type: 'TableMetadatas', id: name }]),
     );
-    dispatch(syncTable(table, tableData));
+    dispatch(syncTable(table, tableData, table.queryEditorId));
   };
 
   const renderWell = () => {
diff --git a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js
deleted file mode 100644
index 9fa11de0da..0000000000
--- a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.js
+++ /dev/null
@@ -1,136 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import persistState from 'redux-localstorage';
-import { pickBy } from 'lodash';
-import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
-import { filterUnsavedQueryEditorList } from 'src/SqlLab/components/EditorAutoSync';
-import {
-  emptyTablePersistData,
-  emptyQueryResults,
-  clearQueryEditors,
-} from '../utils/reduxStateToLocalStorageHelper';
-import { BYTES_PER_CHAR, KB_STORAGE } from '../constants';
-
-const CLEAR_ENTITY_HELPERS_MAP = {
-  tables: emptyTablePersistData,
-  queries: emptyQueryResults,
-  queryEditors: clearQueryEditors,
-  unsavedQueryEditor: qe => clearQueryEditors([qe])[0],
-};
-
-const sqlLabPersistStateConfig = {
-  paths: ['sqlLab'],
-  config: {
-    slicer: paths => state => {
-      const subset = {};
-      paths.forEach(path => {
-        if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) {
-          const {
-            queryEditors,
-            editorTabLastUpdatedAt,
-            unsavedQueryEditor,
-            tables,
-            queries,
-            tabHistory,
-            lastUpdatedActiveTab,
-            destroyedQueryEditors,
-          } = state.sqlLab;
-          const unsavedQueryEditors = filterUnsavedQueryEditorList(
-            queryEditors,
-            unsavedQueryEditor,
-            editorTabLastUpdatedAt,
-          );
-          const hasUnsavedActiveTabState =
-            tabHistory.slice(-1)[0] !== lastUpdatedActiveTab;
-          const hasUnsavedDeletedQueryEditors =
-            Object.keys(destroyedQueryEditors).length > 0;
-          if (
-            unsavedQueryEditors.length > 0 ||
-            hasUnsavedActiveTabState ||
-            hasUnsavedDeletedQueryEditors
-          ) {
-            const hasFinishedMigrationFromLocalStorage =
-              unsavedQueryEditors.every(
-                ({ inLocalStorage }) => !inLocalStorage,
-              );
-            subset.sqlLab = {
-              queryEditors: unsavedQueryEditors,
-              ...(!hasFinishedMigrationFromLocalStorage && {
-                tabHistory,
-                tables: tables.filter(table => table.inLocalStorage),
-                queries: pickBy(
-                  queries,
-                  query => query.inLocalStorage && !query.isDataPreview,
-                ),
-              }),
-              ...(hasUnsavedActiveTabState && {
-                tabHistory,
-              }),
-              destroyedQueryEditors,
-            };
-          }
-          return;
-        }
-        // this line is used to remove old data from browser localStorage.
-        // we used to persist all redux state into localStorage, but
-        // it caused configurations passed from server-side got override.
-        // see PR 6257 for details
-        delete state[path].common; // eslint-disable-line no-param-reassign
-        if (path === 'sqlLab') {
-          subset[path] = Object.fromEntries(
-            Object.entries(state[path]).map(([key, value]) => [
-              key,
-              CLEAR_ENTITY_HELPERS_MAP[key]?.(value) ?? value,
-            ]),
-          );
-        }
-      });
-
-      const data = JSON.stringify(subset);
-      // 2 digit precision
-      const currentSize =
-        Math.round(((data.length * BYTES_PER_CHAR) / KB_STORAGE) * 100) / 100;
-      if (state.localStorageUsageInKilobytes !== currentSize) {
-        state.localStorageUsageInKilobytes = currentSize; // eslint-disable-line no-param-reassign
-      }
-
-      return subset;
-    },
-    merge: (initialState, persistedState = {}) => {
-      const result = {
-        ...initialState,
-        ...persistedState,
-        sqlLab: {
-          ...persistedState?.sqlLab,
-          // Overwrite initialState over persistedState for sqlLab
-          // since a logic in getInitialState overrides the value from persistedState
-          ...initialState.sqlLab,
-        },
-      };
-      return result;
-    },
-  },
-};
-
-// TODO: requires redux-localstorage > 1.0 for typescript support
-/** @type {any} */
-export const persistSqlLabStateEnhancer = persistState(
-  sqlLabPersistStateConfig.paths,
-  sqlLabPersistStateConfig.config,
-);
diff --git a/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts
new file mode 100644
index 0000000000..730482cf6b
--- /dev/null
+++ b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts
@@ -0,0 +1,185 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type { StoreEnhancer } from 'redux';
+import persistState from 'redux-localstorage';
+import { pickBy } from 'lodash';
+import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
+import { filterUnsavedQueryEditorList } from 'src/SqlLab/components/EditorAutoSync';
+import type {
+  SqlLabRootState,
+  QueryEditor,
+  UnsavedQueryEditor,
+  Table,
+} from '../types';
+import {
+  emptyTablePersistData,
+  emptyQueryResults,
+  clearQueryEditors,
+} from '../utils/reduxStateToLocalStorageHelper';
+import { BYTES_PER_CHAR, KB_STORAGE } from '../constants';
+
+type SqlLabState = SqlLabRootState['sqlLab'];
+
+type ClearEntityHelperValue =
+  | Table[]
+  | SqlLabState['queries']
+  | QueryEditor[]
+  | UnsavedQueryEditor;
+
+interface ClearEntityHelpersMap {
+  tables: (tables: Table[]) => ReturnType<typeof emptyTablePersistData>;
+  queries: (
+    queries: SqlLabState['queries'],
+  ) => ReturnType<typeof emptyQueryResults>;
+  queryEditors: (
+    queryEditors: QueryEditor[],
+  ) => ReturnType<typeof clearQueryEditors>;
+  unsavedQueryEditor: (
+    qe: UnsavedQueryEditor,
+  ) => ReturnType<typeof clearQueryEditors>[number];
+}
+
+const CLEAR_ENTITY_HELPERS_MAP: ClearEntityHelpersMap = {
+  tables: emptyTablePersistData,
+  queries: emptyQueryResults,
+  queryEditors: clearQueryEditors,
+  unsavedQueryEditor: (qe: UnsavedQueryEditor) =>
+    clearQueryEditors([qe as QueryEditor])[0],
+};
+
+interface PersistedSqlLabState {
+  sqlLab?: Partial<SqlLabState>;
+  localStorageUsageInKilobytes?: number;
+}
+
+const sqlLabPersistStateConfig = {
+  paths: ['sqlLab'],
+  config: {
+    slicer:
+      (paths: string[]) =>
+      (state: SqlLabRootState): PersistedSqlLabState => {
+        const subset: PersistedSqlLabState = {};
+        paths.forEach(path => {
+          if (isFeatureEnabled(FeatureFlag.SqllabBackendPersistence)) {
+            const {
+              queryEditors,
+              editorTabLastUpdatedAt,
+              unsavedQueryEditor,
+              tables,
+              queries,
+              tabHistory,
+              lastUpdatedActiveTab,
+              destroyedQueryEditors,
+            } = state.sqlLab;
+            const unsavedQueryEditors = filterUnsavedQueryEditorList(
+              queryEditors,
+              unsavedQueryEditor,
+              editorTabLastUpdatedAt,
+            );
+            const hasUnsavedActiveTabState =
+              tabHistory.slice(-1)[0] !== lastUpdatedActiveTab;
+            const hasUnsavedDeletedQueryEditors =
+              Object.keys(destroyedQueryEditors).length > 0;
+            if (
+              unsavedQueryEditors.length > 0 ||
+              hasUnsavedActiveTabState ||
+              hasUnsavedDeletedQueryEditors
+            ) {
+              const hasFinishedMigrationFromLocalStorage =
+                unsavedQueryEditors.every(
+                  ({ inLocalStorage }) => !inLocalStorage,
+                );
+              subset.sqlLab = {
+                queryEditors: unsavedQueryEditors,
+                ...(!hasFinishedMigrationFromLocalStorage && {
+                  tabHistory,
+                  tables: tables.filter(table => table.inLocalStorage),
+                  queries: pickBy(
+                    queries,
+                    query => query.inLocalStorage && !query.isDataPreview,
+                  ),
+                }),
+                ...(hasUnsavedActiveTabState && {
+                  tabHistory,
+                }),
+                destroyedQueryEditors,
+              };
+            }
+            return;
+          }
+          // this line is used to remove old data from browser localStorage.
+          // we used to persist all redux state into localStorage, but
+          // it caused configurations passed from server-side got override.
+          // see PR 6257 for details
+          const statePath = state[path as keyof SqlLabRootState];
+          if (
+            statePath &&
+            typeof statePath === 'object' &&
+            'common' in statePath
+          ) {
+            delete (statePath as Record<string, unknown>).common; // eslint-disable-line no-param-reassign
+          }
+          if (path === 'sqlLab') {
+            subset[path] = Object.fromEntries(
+              Object.entries(state[path]).map(([key, value]) => {
+                const helper = CLEAR_ENTITY_HELPERS_MAP[
+                  key as keyof ClearEntityHelpersMap
+                ] as ((val: ClearEntityHelperValue) => unknown) | undefined;
+                return [
+                  key,
+                  helper?.(value as ClearEntityHelperValue) ?? value,
+                ];
+              }),
+            );
+          }
+        });
+
+        const data = JSON.stringify(subset);
+        // 2 digit precision
+        const currentSize =
+          Math.round(((data.length * BYTES_PER_CHAR) / KB_STORAGE) * 100) / 100;
+        if (state.localStorageUsageInKilobytes !== currentSize) {
+          state.localStorageUsageInKilobytes = currentSize; // eslint-disable-line no-param-reassign
+        }
+
+        return subset;
+      },
+    merge: (
+      initialState: SqlLabRootState,
+      persistedState: PersistedSqlLabState = {},
+    ) => ({
+      ...initialState,
+      ...persistedState,
+      sqlLab: {
+        ...persistedState?.sqlLab,
+        // Overwrite initialState over persistedState for sqlLab
+        // since a logic in getInitialState overrides the value from persistedState
+        ...initialState.sqlLab,
+      },
+    }),
+  },
+};
+
+// redux-localstorage doesn't have TypeScript definitions
+// The library returns a StoreEnhancer that persists specified paths to localStorage
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const persistSqlLabStateEnhancer = (persistState as any)(
+  sqlLabPersistStateConfig.paths,
+  sqlLabPersistStateConfig.config,
+) as StoreEnhancer;
diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.ts
similarity index 67%
rename from superset-frontend/src/SqlLab/reducers/sqlLab.js
rename to superset-frontend/src/SqlLab/reducers/sqlLab.ts
index 3469036da4..55981f0aa5 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.ts
@@ -20,7 +20,9 @@ import { normalizeTimestamp, QueryState, t } from '@superset-ui/core';
 import { isEqual, omit } from 'lodash';
 import { shallowEqual } from 'react-redux';
 import { now } from '@superset-ui/core/utils/dates';
+import type { SqlLabRootState, QueryEditor, Table } from '../types';
 import * as actions from '../actions/sqlLab';
+import type { SqlLabAction } from '../actions/sqlLab';
 import {
   addToObject,
   alterInObject,
@@ -31,7 +33,14 @@ import {
   extendArr,
 } from '../../reduxUtils';
 
-function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) {
+type SqlLabState = SqlLabRootState['sqlLab'];
+
+function alterUnsavedQueryEditorState(
+  state: SqlLabState,
+  updatedState: Partial<QueryEditor>,
+  id: string,
+  silent = false,
+): Partial<SqlLabState> {
   if (state.tabHistory[state.tabHistory.length - 1] !== id) {
     const { queryEditors } = alterInArr(
       state,
@@ -52,8 +61,12 @@ function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) {
   };
 }
 
-export default function sqlLabReducer(state = {}, action) {
-  const actionHandlers = {
+export default function sqlLabReducer(
+  state: SqlLabState = {} as SqlLabState,
+  action: SqlLabAction,
+): SqlLabState {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const actionHandlers: Record<string, () => any> = {
     [actions.ADD_QUERY_EDITOR]() {
       const mergeUnsavedState = alterInArr(
         state,
@@ -65,10 +78,10 @@ export default function sqlLabReducer(state = {}, action) {
       );
       const newState = {
         ...mergeUnsavedState,
-        tabHistory: [...state.tabHistory, action.queryEditor.id],
+        tabHistory: [...state.tabHistory, action.queryEditor!.id],
       };
       return addToArr(newState, 'queryEditors', {
-        ...action.queryEditor,
+        ...action.queryEditor!,
         updatedAt: new Date().getTime(),
       });
     },
@@ -78,23 +91,24 @@ export default function sqlLabReducer(state = {}, action) {
       return alterInArr(
         state,
         'queryEditors',
-        existing,
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        existing as any,
         {
-          remoteId: result.remoteId,
-          name: query.name,
+          remoteId: result!.remoteId,
+          name: (query as { name: string }).name,
         },
         'id',
       );
     },
     [actions.UPDATE_QUERY_EDITOR]() {
-      const id = action.alterations.remoteId;
+      const id = action.alterations!.remoteId;
       const existing = state.queryEditors.find(qe => qe.remoteId === id);
       if (existing == null) return state;
       return alterInArr(
         state,
         'queryEditors',
         existing,
-        action.alterations,
+        action.alterations!,
         'remoteId',
       );
     },
@@ -104,19 +118,20 @@ export default function sqlLabReducer(state = {}, action) {
       );
       const progenitor = {
         ...queryEditor,
-        ...(state.unsavedQueryEditor.id === queryEditor.id &&
+        ...(state.unsavedQueryEditor.id === queryEditor?.id &&
           state.unsavedQueryEditor),
       };
       const qe = {
         remoteId: progenitor.remoteId,
         name: t('Copy of %s', progenitor.name),
-        dbId: action.query.dbId ? action.query.dbId : null,
-        catalog: action.query.catalog ? action.query.catalog : null,
-        schema: action.query.schema ? action.query.schema : null,
+        dbId: action.query!.dbId ? action.query!.dbId : null,
+        catalog: action.query!.catalog ? action.query!.catalog : null,
+        schema: action.query!.schema ? action.query!.schema : null,
         autorun: true,
-        sql: action.query.sql,
-        queryLimit: action.query.queryLimit,
-        maxRow: action.query.maxRow,
+        sql: action.query!.sql,
+        queryLimit: action.query!.queryLimit,
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        maxRow: (action.query as any)?.maxRow,
       };
       const stateWithoutUnsavedState = {
         ...state,
@@ -124,20 +139,24 @@ export default function sqlLabReducer(state = {}, action) {
       };
       return sqlLabReducer(
         stateWithoutUnsavedState,
-        actions.addQueryEditor(qe),
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        actions.addQueryEditor(qe as any),
       );
     },
     [actions.REMOVE_QUERY_EDITOR]() {
       const queryEditor = {
-        ...action.queryEditor,
-        ...(action.queryEditor.id === state.unsavedQueryEditor.id &&
+        ...action.queryEditor!,
+        ...(action.queryEditor!.id === state.unsavedQueryEditor.id &&
           state.unsavedQueryEditor),
       };
       let newState = removeFromArr(state, 'queryEditors', queryEditor);
       // List of remaining queryEditor ids
-      const qeIds = newState.queryEditors.map(qe => qe.tabViewId ?? qe.id);
+      const qeIds = newState.queryEditors.map(
+        (qe: QueryEditor) => qe.tabViewId ?? qe.id,
+      );
 
-      const queries = {};
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const queries: any = {};
       Object.keys(state.queries).forEach(k => {
         const query = state.queries[k];
         if (qeIds.indexOf(query.sqlEditorId) > -1) {
@@ -158,18 +177,18 @@ export default function sqlLabReducer(state = {}, action) {
         ...newState,
         tabHistory:
           tabHistory.length === 0 && newState.queryEditors.length > 0
-            ? newState.queryEditors.slice(-1).map(qe => qe.id)
+            ? newState.queryEditors.slice(-1).map((qe: QueryEditor) => qe.id)
             : tabHistory,
         tables,
         queries,
         unsavedQueryEditor: {
-          ...(action.queryEditor.id !== state.unsavedQueryEditor.id &&
+          ...(action.queryEditor!.id !== state.unsavedQueryEditor.id &&
             state.unsavedQueryEditor),
         },
         destroyedQueryEditors: {
           ...newState.destroyedQueryEditors,
           ...(!queryEditor.inLocalStorage && {
-            [queryEditor.tabViewId ?? queryEditor.id]: Date.now(),
+            [(queryEditor.tabViewId ?? queryEditor.id)!]: Date.now(),
           }),
         },
       };
@@ -177,19 +196,19 @@ export default function sqlLabReducer(state = {}, action) {
     },
     [actions.CLEAR_DESTROYED_QUERY_EDITOR]() {
       const destroyedQueryEditors = { ...state.destroyedQueryEditors };
-      delete destroyedQueryEditors[action.queryEditorId];
+      delete destroyedQueryEditors[action.queryEditorId!];
       return { ...state, destroyedQueryEditors };
     },
     [actions.REMOVE_QUERY]() {
       const newQueries = { ...state.queries };
-      delete newQueries[action.query.id];
+      delete newQueries[action.query!.id!];
       return { ...state, queries: newQueries };
     },
     [actions.RESET_STATE]() {
       return { ...action.sqlLabInitialState };
     },
     [actions.MERGE_TABLE]() {
-      const at = { ...action.table };
+      const at = { ...action.table } as Table;
       const existingTableIndex = state.tables.findIndex(
         xt =>
           xt.dbId === at.dbId &&
@@ -200,7 +219,7 @@ export default function sqlLabReducer(state = {}, action) {
       );
       if (existingTableIndex >= 0) {
         if (action.query) {
-          at.dataPreviewQueryId = action.query.id;
+          at.dataPreviewQueryId = action.query!.id;
         }
         return {
           ...state,
@@ -228,30 +247,30 @@ export default function sqlLabReducer(state = {}, action) {
       };
       if (action.query) {
         newState = alterInArr(newState, 'tables', at, {
-          dataPreviewQueryId: action.query.id,
+          dataPreviewQueryId: action.query!.id,
         });
       }
       return newState;
     },
     [actions.EXPAND_TABLE]() {
-      return alterInArr(state, 'tables', action.table, { expanded: true });
+      return alterInArr(state, 'tables', action.table!, { expanded: true });
     },
     [actions.REMOVE_DATA_PREVIEW]() {
       const queries = { ...state.queries };
-      delete queries[action.table.dataPreviewQueryId];
-      const newState = alterInArr(state, 'tables', action.table, {
+      delete queries[action.table!.dataPreviewQueryId!];
+      const newState = alterInArr(state, 'tables', action.table!, {
         dataPreviewQueryId: null,
       });
       return { ...newState, queries };
     },
     [actions.CHANGE_DATA_PREVIEW_ID]() {
       const queries = { ...state.queries };
-      delete queries[action.oldQueryId];
+      delete queries[action.oldQueryId!];
 
-      const newTables = [];
+      const newTables: Table[] = [];
       state.tables.forEach(xt => {
         if (xt.dataPreviewQueryId === action.oldQueryId) {
-          newTables.push({ ...xt, dataPreviewQueryId: action.newQuery.id });
+          newTables.push({ ...xt, dataPreviewQueryId: action.newQuery!.id });
         } else {
           newTables.push(xt);
         }
@@ -263,20 +282,20 @@ export default function sqlLabReducer(state = {}, action) {
       };
     },
     [actions.COLLAPSE_TABLE]() {
-      return alterInArr(state, 'tables', action.table, { expanded: false });
+      return alterInArr(state, 'tables', action.table!, { expanded: false });
     },
     [actions.REMOVE_TABLES]() {
-      const tableIds = action.tables.map(table => table.id);
+      const tableIds = action.tables!.map((table: Table) => table.id);
       const tables = state.tables.filter(table => !tableIds.includes(table.id));
 
       return {
         ...state,
         tables,
-        ...(tableIds.includes(state.activeSouthPaneTab) && {
+        ...(tableIds.includes(state.activeSouthPaneTab as string) && {
           activeSouthPaneTab:
             tables.find(
               ({ queryEditorId }) =>
-                queryEditorId === action.tables[0].queryEditorId,
+                queryEditorId === action.tables![0].queryEditorId,
             )?.id ?? 'Results',
         }),
       };
@@ -286,7 +305,7 @@ export default function sqlLabReducer(state = {}, action) {
         ...state,
         queryCostEstimates: {
           ...state.queryCostEstimates,
-          [action.query.id]: {
+          [action.query!.id!]: {
             completed: false,
             cost: null,
             error: null,
@@ -299,9 +318,9 @@ export default function sqlLabReducer(state = {}, action) {
         ...state,
         queryCostEstimates: {
           ...state.queryCostEstimates,
-          [action.query.id]: {
+          [action.query!.id!]: {
             completed: true,
-            cost: action.json.result,
+            cost: action.json!.result,
             error: null,
           },
         },
@@ -312,7 +331,7 @@ export default function sqlLabReducer(state = {}, action) {
         ...state,
         queryCostEstimates: {
           ...state.queryCostEstimates,
-          [action.query.id]: {
+          [action.query!.id!]: {
             completed: false,
             cost: null,
             error: action.error,
@@ -323,15 +342,19 @@ export default function sqlLabReducer(state = {}, action) {
     [actions.START_QUERY]() {
       let newState = { ...state };
       let sqlEditorId;
-      if (action.query.sqlEditorId) {
+      if (action.query!.sqlEditorId) {
         const queryEditorByTabId = getFromArr(
           state.queryEditors,
-          action.query.sqlEditorId,
+          action.query!.sqlEditorId,
           'tabViewId',
         );
-        sqlEditorId = queryEditorByTabId?.id ?? action.query.sqlEditorId;
+        sqlEditorId =
+          (queryEditorByTabId as QueryEditor | undefined)?.id ??
+          action.query!.sqlEditorId;
+        const foundQueryEditor = getFromArr(state.queryEditors, sqlEditorId);
+        const baseQe = foundQueryEditor || {};
         const qe = {
-          ...getFromArr(state.queryEditors, sqlEditorId),
+          ...baseQe,
           ...(sqlEditorId === state.unsavedQueryEditor.id &&
             state.unsavedQueryEditor),
         };
@@ -342,40 +365,44 @@ export default function sqlLabReducer(state = {}, action) {
             query: null,
           };
           const q = { ...state.queries[qe.latestQueryId], results: newResults };
-          const queries = { ...state.queries, [q.id]: q };
+          const queries = {
+            ...state.queries,
+            [q.id]: q,
+          } as SqlLabState['queries'];
           newState = { ...state, queries };
         }
       }
-      newState = addToObject(newState, 'queries', action.query);
+      newState = addToObject(newState, 'queries', action.query!) as SqlLabState;
 
       return {
         ...newState,
         ...alterUnsavedQueryEditorState(
           state,
           {
-            latestQueryId: action.query.id,
+            latestQueryId: action.query!.id,
           },
-          sqlEditorId,
-          action.query.isDataPreview,
+          sqlEditorId!,
+          action.query!.isDataPreview,
         ),
       };
     },
     [actions.STOP_QUERY]() {
-      return alterInObject(state, 'queries', action.query, {
+      return alterInObject(state, 'queries', action.query!, {
         state: QueryState.Stopped,
         results: [],
       });
     },
     [actions.CLEAR_QUERY_RESULTS]() {
-      const newResults = { ...action.query.results };
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const newResults = { ...(action.query as any).results };
       newResults.data = [];
-      return alterInObject(state, 'queries', action.query, {
+      return alterInObject(state, 'queries', action.query!, {
         results: newResults,
         cached: true,
       });
     },
     [actions.REQUEST_QUERY_RESULTS]() {
-      return alterInObject(state, 'queries', action.query, {
+      return alterInObject(state, 'queries', action.query!, {
         state: QueryState.Fetching,
       });
     },
@@ -383,12 +410,13 @@ export default function sqlLabReducer(state = {}, action) {
       // prevent race condition where query succeeds shortly after being canceled
       // or the final result was unsuccessful
       if (
-        action.query.state === QueryState.STOPPED ||
-        action.results.status !== QueryState.Success
+        action.query!.state === QueryState.Stopped ||
+        (action.results as { status?: string })?.status !== QueryState.Success
       ) {
         return state;
       }
-      const alts = {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const alts: any = {
         endDttm: now(),
         progress: 100,
         results: action.results,
@@ -407,10 +435,10 @@ export default function sqlLabReducer(state = {}, action) {
         alts.resultsKey = resultsKey;
       }
 
-      return alterInObject(state, 'queries', action.query, alts);
+      return alterInObject(state, 'queries', action.query!, alts);
     },
     [actions.QUERY_FAILED]() {
-      if (action.query.state === QueryState.Stopped) {
+      if (action.query!.state === QueryState.Stopped) {
         return state;
       }
       const alts = {
@@ -420,13 +448,13 @@ export default function sqlLabReducer(state = {}, action) {
         endDttm: now(),
         link: action.link,
       };
-      return alterInObject(state, 'queries', action.query, alts);
+      return alterInObject(state, 'queries', action.query!, alts);
     },
     [actions.SET_ACTIVE_QUERY_EDITOR]() {
       const qeIds = state.queryEditors.map(qe => qe.id);
       if (
-        qeIds.indexOf(action.queryEditor?.id) > -1 &&
-        state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor.id
+        qeIds.indexOf(action.queryEditor!.id!) > -1 &&
+        state.tabHistory[state.tabHistory.length - 1] !== action.queryEditor!.id
       ) {
         const mergeUnsavedState = {
           ...alterInArr(state, 'queryEditors', state.unsavedQueryEditor, {
@@ -435,18 +463,18 @@ export default function sqlLabReducer(state = {}, action) {
           unsavedQueryEditor: {},
         };
         return {
-          ...(action.queryEditor.id === state.unsavedQueryEditor.id
+          ...(action.queryEditor!.id === state.unsavedQueryEditor.id
             ? alterInArr(
                 mergeUnsavedState,
                 'queryEditors',
-                action.queryEditor,
+                action.queryEditor!,
                 {
-                  ...action.queryEditor,
+                  ...action.queryEditor!,
                   ...state.unsavedQueryEditor,
                 },
               )
             : mergeUnsavedState),
-          tabHistory: [...state.tabHistory, action.queryEditor.id],
+          tabHistory: [...state.tabHistory, action.queryEditor!.id],
         };
       }
       return state;
@@ -460,12 +488,17 @@ export default function sqlLabReducer(state = {}, action) {
           ...state.unsavedQueryEditor,
         },
       );
-      return alterInArr(mergeUnsavedState, 'queryEditors', action.queryEditor, {
-        ...action.queryEditor,
-      });
+      return alterInArr(
+        mergeUnsavedState,
+        'queryEditors',
+        action.queryEditor!,
+        {
+          ...action.queryEditor!,
+        },
+      );
     },
     [actions.SET_TABLES]() {
-      return extendArr(state, 'tables', action.tables);
+      return extendArr(state, 'tables', action.tables!);
     },
     [actions.SET_ACTIVE_SOUTHPANE_TAB]() {
       return { ...state, activeSouthPaneTab: action.tabId };
@@ -473,9 +506,9 @@ export default function sqlLabReducer(state = {}, action) {
     [actions.MIGRATE_QUERY_EDITOR]() {
       try {
         // remove migrated query editor from localStorage
-        const { sqlLab } = JSON.parse(localStorage.getItem('redux'));
+        const { sqlLab } = JSON.parse(localStorage.getItem('redux') || '{}');
         sqlLab.queryEditors = sqlLab.queryEditors.filter(
-          qe => qe.id !== action.oldQueryEditor.id,
+          (qe: QueryEditor) => qe.id !== action.oldQueryEditor!.id,
         );
         localStorage.setItem('redux', JSON.stringify({ sqlLab }));
       } catch (error) {
@@ -485,16 +518,16 @@ export default function sqlLabReducer(state = {}, action) {
       return alterInArr(
         state,
         'queryEditors',
-        action.oldQueryEditor,
-        action.newQueryEditor,
+        action.oldQueryEditor!,
+        action.newQueryEditor!,
       );
     },
     [actions.MIGRATE_TABLE]() {
       try {
         // remove migrated table from localStorage
-        const { sqlLab } = JSON.parse(localStorage.getItem('redux'));
+        const { sqlLab } = JSON.parse(localStorage.getItem('redux') || '{}');
         sqlLab.tables = sqlLab.tables.filter(
-          table => table.id !== action.oldTable.id,
+          (table: Table) => table.id !== action.oldTable!.id,
         );
         localStorage.setItem('redux', JSON.stringify({ sqlLab }));
       } catch (error) {
@@ -503,14 +536,14 @@ export default function sqlLabReducer(state = {}, action) {
 
       // replace localStorage table with the server backed one
       return addToArr(
-        removeFromArr(state, 'tables', action.oldTable),
+        removeFromArr(state, 'tables', action.oldTable!),
         'tables',
-        action.newTable,
+        action.newTable!,
       );
     },
     [actions.MIGRATE_QUERY]() {
       const query = {
-        ...state.queries[action.queryId],
+        ...state.queries[action.queryId!],
         // point query to migrated query editor
         sqlEditorId: action.queryEditorId,
       };
@@ -525,7 +558,7 @@ export default function sqlLabReducer(state = {}, action) {
           {
             dbId: action.dbId,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -537,7 +570,7 @@ export default function sqlLabReducer(state = {}, action) {
           {
             catalog: action.catalog,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -547,9 +580,9 @@ export default function sqlLabReducer(state = {}, action) {
         ...alterUnsavedQueryEditorState(
           state,
           {
-            schema: action.schema,
+            schema: action.schema ?? undefined,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -561,14 +594,14 @@ export default function sqlLabReducer(state = {}, action) {
           {
             name: action.name,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
     [actions.QUERY_EDITOR_SET_SQL]() {
       const { unsavedQueryEditor } = state;
       if (
-        unsavedQueryEditor?.id === action.queryEditor.id &&
+        unsavedQueryEditor?.id === action.queryEditor!.id &&
         unsavedQueryEditor.sql === action.sql
       ) {
         return state;
@@ -578,10 +611,10 @@ export default function sqlLabReducer(state = {}, action) {
         ...alterUnsavedQueryEditorState(
           state,
           {
-            sql: action.sql,
+            sql: action.sql ?? undefined,
             ...(action.queryId && { latestQueryId: action.queryId }),
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -593,7 +626,7 @@ export default function sqlLabReducer(state = {}, action) {
           {
             cursorPosition: action.position,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -605,7 +638,7 @@ export default function sqlLabReducer(state = {}, action) {
           {
             queryLimit: action.queryLimit,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -617,7 +650,7 @@ export default function sqlLabReducer(state = {}, action) {
           {
             templateParams: action.templateParams,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -627,9 +660,9 @@ export default function sqlLabReducer(state = {}, action) {
         ...alterUnsavedQueryEditorState(
           state,
           {
-            selectedText: action.sql,
+            selectedText: action.sql ?? undefined,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
           true,
         ),
       };
@@ -642,7 +675,7 @@ export default function sqlLabReducer(state = {}, action) {
           {
             autorun: action.autorun,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -655,7 +688,7 @@ export default function sqlLabReducer(state = {}, action) {
             northPercent: action.northPercent,
             southPercent: action.southPercent,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
@@ -667,13 +700,15 @@ export default function sqlLabReducer(state = {}, action) {
           {
             hideLeftBar: action.hideLeftBar,
           },
-          action.queryEditor.id,
+          action.queryEditor!.id!,
         ),
       };
     },
     [actions.SET_DATABASES]() {
-      const databases = {};
-      action.databases.forEach(db => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const databases: any = {};
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      (action.databases as any[])!.forEach((db: any) => {
         databases[db.id] = {
           ...db,
           extra_json: JSON.parse(db.extra || ''),
@@ -686,54 +721,57 @@ export default function sqlLabReducer(state = {}, action) {
       // Fetch the updates to the queries present in the store.
       let change = false;
       let { queriesLastUpdate } = state;
-      Object.entries(action.alteredQueries).forEach(([id, changedQuery]) => {
-        if (
-          !state.queries.hasOwnProperty(id) ||
-          (state.queries[id].state !== QueryState.Stopped &&
-            state.queries[id].state !== QueryState.Failed)
-        ) {
-          const changedOn = normalizeTimestamp(changedQuery.changed_on);
-          const timestamp = Date.parse(changedOn);
-          if (timestamp > queriesLastUpdate) {
-            queriesLastUpdate = timestamp;
-          }
-          const prevState = state.queries[id]?.state;
-          const currentState = changedQuery.state;
-          newQueries[id] = {
-            ...state.queries[id],
-            ...changedQuery,
-            ...(changedQuery.startDttm && {
-              startDttm: Number(changedQuery.startDttm),
-            }),
-            ...(changedQuery.endDttm && {
-              endDttm: Number(changedQuery.endDttm),
-            }),
-            // race condition:
-            // because of async behavior, sql lab may still poll a couple of seconds
-            // when it started fetching or finished rendering results
-            state:
-              currentState === QueryState.Success &&
-              [
-                QueryState.Fetching,
-                QueryState.Success,
-                QueryState.Running,
-              ].includes(prevState)
-                ? prevState
-                : currentState,
-          };
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      Object.entries(action.alteredQueries!).forEach(
+        ([id, changedQuery]: [string, any]) => {
           if (
-            shallowEqual(
-              omit(newQueries[id], ['extra']),
-              omit(state.queries[id], ['extra']),
-            ) &&
-            isEqual(newQueries[id].extra, state.queries[id].extra)
+            !state.queries.hasOwnProperty(id) ||
+            (state.queries[id].state !== QueryState.Stopped &&
+              state.queries[id].state !== QueryState.Failed)
           ) {
-            newQueries[id] = state.queries[id];
-          } else {
-            change = true;
+            const changedOn = normalizeTimestamp(changedQuery.changed_on);
+            const timestamp = Date.parse(changedOn);
+            if (timestamp > queriesLastUpdate) {
+              queriesLastUpdate = timestamp;
+            }
+            const prevState = state.queries[id]?.state;
+            const currentState = changedQuery.state;
+            newQueries[id] = {
+              ...state.queries[id],
+              ...changedQuery,
+              ...(changedQuery.startDttm && {
+                startDttm: Number(changedQuery.startDttm),
+              }),
+              ...(changedQuery.endDttm && {
+                endDttm: Number(changedQuery.endDttm),
+              }),
+              // race condition:
+              // because of async behavior, sql lab may still poll a couple of seconds
+              // when it started fetching or finished rendering results
+              state:
+                currentState === QueryState.Success &&
+                [
+                  QueryState.Fetching,
+                  QueryState.Success,
+                  QueryState.Running,
+                ].includes(prevState)
+                  ? prevState
+                  : currentState,
+            };
+            if (
+              shallowEqual(
+                omit(newQueries[id], ['extra']),
+                omit(state.queries[id], ['extra']),
+              ) &&
+              isEqual(newQueries[id].extra, state.queries[id].extra)
+            ) {
+              newQueries[id] = state.queries[id];
+            } else {
+              change = true;
+            }
           }
-        }
-      });
+        },
+      );
       if (!change) {
         newQueries = state.queries;
       }
@@ -746,14 +784,15 @@ export default function sqlLabReducer(state = {}, action) {
           .filter(([, query]) => {
             if (
               ['running', 'pending'].includes(query.state) &&
-              Date.now() - query.startDttm > action.interval &&
+              Date.now() - query.startDttm > action.interval! &&
               query.progress === 0
             ) {
               return false;
             }
             return true;
           })
-          .map(([id, query]) => [
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          .map(([id, query]: [string, any]) => [
             id,
             {
               ...query,
diff --git a/superset-frontend/src/core/sqlLab/index.ts b/superset-frontend/src/core/sqlLab/index.ts
index d218fab2c1..dbadd95e45 100644
--- a/superset-frontend/src/core/sqlLab/index.ts
+++ b/superset-frontend/src/core/sqlLab/index.ts
@@ -155,28 +155,32 @@ function createQueryResultContext(
   action: ReturnType<typeof querySuccess>,
 ): QueryResultContext {
   const { baseParams, options } = extractBaseData(action);
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
   const [_, tab] = baseParams;
   const { results } = action;
+  const { query_id: queryId, columns, data, query } = results;
   const {
-    query_id: queryId,
-    columns,
-    data,
-    query: {
-      endDttm,
-      executedSql,
-      tempTable: resultTempTable,
-      limit,
-      limitingFactor,
-    },
-  } = results;
+    endDttm,
+    executedSql,
+    tempTable: resultTempTable,
+    limit,
+    limitingFactor,
+  } = query;
+
+  // Map columns to ensure required fields are present
+  const mappedColumns = columns.map(col => ({
+    ...col,
+    name: col.name || col.column_name,
+    type: col.type ?? 'STRING', // Ensure type is not null
+  }));
 
   return new QueryResultContext(
     ...baseParams,
-    queryId,
+    queryId ?? 0,
     executedSql ?? tab.editor.content,
-    columns,
+    mappedColumns,
     data,
-    endDttm,
+    endDttm ?? 0,
     {
       ...options,
       tempTable: resultTempTable || options.tempTable,
@@ -193,12 +197,23 @@ function createQueryErrorContext(
   const { msg: errorMessage, errors, query } = action;
   const { endDttm, executedSql, query_id: queryId } = query;
 
-  return new QueryErrorResultContext(...baseParams, errorMessage, errors, {
-    ...options,
-    queryId,
-    executedSql: executedSql ?? null,
-    endDttm: endDttm ?? Date.now(),
-  });
+  // Map errors to ensure 'extra' is not null (required by QueryErrorResultContext)
+  const mappedErrors = (errors ?? []).map(err => ({
+    ...err,
+    extra: err.extra ?? {},
+  }));
+
+  return new QueryErrorResultContext(
+    ...baseParams,
+    errorMessage,
+    mappedErrors,
+    {
+      ...options,
+      queryId,
+      executedSql: executedSql ?? undefined,
+      endDttm: endDttm ?? Date.now(),
+    },
+  );
 }
 
 const getCurrentTab: typeof sqlLabType.getCurrentTab = () =>
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 2c740d771e..26162db2d1 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -19,7 +19,6 @@
 /* eslint-disable react-hooks/rules-of-hooks */
 import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
 import {
-  AdhocFilter,
   Behavior,
   ChartDataResponseResult,
   Column,
@@ -78,6 +77,7 @@ import {
 } from 'src/dashboard/types';
 import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
 import AdhocFilterControl from 'src/explore/components/controls/FilterControl/AdhocFilterControl';
+import type AdhocFilterClass from 'src/explore/components/controls/FilterControl/AdhocFilter';
 import { waitForAsyncData } from 'src/middleware/asyncEvent';
 import { SingleValueType } from 'src/filters/components/Range/SingleValueType';
 import { RangeDisplayMode } from 'src/filters/components/Range/types';
@@ -1003,7 +1003,7 @@ const FiltersConfigForm = (
                                           }
                                           datasource={datasetDetails}
                                           onChange={(
-                                            filters: AdhocFilter[],
+                                            filters: AdhocFilterClass[],
                                           ) => {
                                             setNativeFilterFieldValues(
                                               form,
diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx
index bec46b6784..0c6780f57d 100644
--- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx
+++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx
@@ -19,6 +19,7 @@
 import {
   render,
   screen,
+  selectOption,
   userEvent,
   waitFor,
 } from 'spec/helpers/testing-library';
@@ -210,39 +211,23 @@ test('fetches chart on mount if value present', async () => {
 });
 
 test('keeps apply disabled when missing required fields', async () => {
+  // With EVENT type and Table source, the component requires selecting a chart
+  // and filling in required fields. Without completing these, Apply should be disabled.
   await waitForRender({
     annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
     sourceType: 'Table',
   });
-  userEvent.click(
-    screen.getByRole('combobox', { name: 'Annotation layer value' }),
-  );
-  expect(await screen.findByText('Chart A')).toBeInTheDocument();
-  userEvent.click(screen.getByText('Chart A'));
-  await screen.findByText(/title column/i);
-  userEvent.click(
-    screen.getByRole('combobox', { name: 'Annotation layer title column' }),
-  );
-  expect(await screen.findByText(/none/i)).toBeInTheDocument();
-  userEvent.click(screen.getByText('None'));
-  userEvent.click(screen.getByText('Style'));
-  // The checkbox for automatic color is in the Style tab
-  userEvent.click(screen.getByText('Use automatic color'));
-  userEvent.click(
-    screen.getByRole('combobox', { name: 'Annotation layer stroke' }),
-  );
-  expect(await screen.findByText('Dashed')).toBeInTheDocument();
-  userEvent.click(screen.getByText('Dashed'));
-  userEvent.click(screen.getByText('Opacity'));
-  userEvent.click(
-    screen.getByRole('combobox', { name: 'Annotation layer opacity' }),
-  );
-  expect(await screen.findByText(/0.5/i)).toBeInTheDocument();
-  userEvent.click(screen.getByText('0.5'));
 
-  const checkboxes = screen.getAllByRole('checkbox');
-  checkboxes.forEach(checkbox => userEvent.click(checkbox));
+  // Apply button should be disabled initially since required fields are not filled
+  expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
+
+  // Select Chart A from the annotation layer value dropdown
+  await selectOption('Chart A', 'Annotation layer value');
+
+  // Wait for the chart data to load
+  await screen.findByText(/title column/i);
 
+  // Apply should still be disabled because name is not filled
   expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
 });
 
diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx
similarity index 74%
rename from superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx
rename to superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx
index f520267d13..bdffa8c14e 100644
--- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx
+++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx
@@ -16,9 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { PureComponent } from 'react';
+import React, { PureComponent } from 'react';
 import rison from 'rison';
-import PropTypes from 'prop-types';
 import {
   Button,
   AsyncSelect,
@@ -34,8 +33,13 @@ import {
   isValidExpression,
   getColumnLabel,
   VizType,
+  type QueryFormColumn,
 } from '@superset-ui/core';
-import { styled, withTheme } from '@apache-superset/core/ui';
+import {
+  styled,
+  withTheme,
+  type SupersetTheme,
+} from '@apache-superset/core/ui';
 import SelectControl from 'src/explore/components/controls/SelectControl';
 import TextControl from 'src/explore/components/controls/TextControl';
 import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
@@ -50,59 +54,80 @@ import {
   ANNOTATION_SOURCE_TYPES_METADATA,
 } from './AnnotationTypes';
 
-const AUTOMATIC_COLOR = '';
+interface SelectOption {
+  value: string | number;
+  label: string;
+  viz_type?: string;
+  [key: string]: unknown;
+}
 
-const propTypes = {
-  name: PropTypes.string,
-  annotationType: PropTypes.string,
-  sourceType: PropTypes.string,
-  color: PropTypes.string,
-  opacity: PropTypes.string,
-  style: PropTypes.string,
-  width: PropTypes.number,
-  showMarkers: PropTypes.bool,
-  hideLine: PropTypes.bool,
-  value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-  overrides: PropTypes.object,
-  show: PropTypes.bool,
-  showLabel: PropTypes.bool,
-  titleColumn: PropTypes.string,
-  descriptionColumns: PropTypes.arrayOf(PropTypes.string),
-  timeColumn: PropTypes.string,
-  intervalEndColumn: PropTypes.string,
-  vizType: PropTypes.string,
+interface SliceData {
+  data: {
+    groupby?: string[];
+    all_columns?: string[];
+    include_time?: boolean;
+    [key: string]: unknown;
+  };
+}
 
-  error: PropTypes.string,
-  colorScheme: PropTypes.string,
+interface AnnotationOverrides {
+  time_range?: string | null;
+  time_grain_sqla?: string | null;
+  granularity?: string | null;
+  time_shift?: string;
+  [key: string]: unknown;
+}
 
-  addAnnotationLayer: PropTypes.func,
-  removeAnnotationLayer: PropTypes.func,
-  close: PropTypes.func,
-};
+interface AnnotationLayerProps {
+  name?: string;
+  annotationType?: string;
+  sourceType?: string;
+  color?: string;
+  opacity?: string;
+  style?: string;
+  width?: number;
+  showMarkers?: boolean;
+  hideLine?: boolean;
+  value?: string | number | SelectOption;
+  overrides?: AnnotationOverrides;
+  show?: boolean;
+  showLabel?: boolean;
+  titleColumn?: string;
+  descriptionColumns?: string[];
+  timeColumn?: string;
+  intervalEndColumn?: string;
+  vizType?: string;
+  error?: string;
+  colorScheme?: string;
+  theme: SupersetTheme;
+  addAnnotationLayer?: (annotation: Record<string, unknown>) => void;
+  removeAnnotationLayer?: () => void;
+  close?: () => void;
+}
 
-const defaultProps = {
-  name: '',
-  annotationType: DEFAULT_ANNOTATION_TYPE,
-  sourceType: '',
-  color: AUTOMATIC_COLOR,
-  opacity: '',
-  style: 'solid',
-  width: 1,
-  showMarkers: false,
-  hideLine: false,
-  overrides: {},
-  colorScheme: 'd3Category10',
-  show: true,
-  showLabel: false,
-  titleColumn: '',
-  descriptionColumns: [],
-  timeColumn: '',
-  intervalEndColumn: '',
+interface AnnotationLayerState {
+  name: string;
+  annotationType: string;
+  sourceType: string | null;
+  value: string | number | SelectOption | null;
+  overrides: AnnotationOverrides;
+  show: boolean;
+  showLabel: boolean;
+  titleColumn: string;
+  descriptionColumns: string[];
+  timeColumn: string;
+  intervalEndColumn: string;
+  color: string;
+  opacity: string;
+  style: string;
+  width: number;
+  showMarkers: boolean;
+  hideLine: boolean;
+  isNew: boolean;
+  slice: SliceData | null;
+}
 
-  addAnnotationLayer: () => {},
-  removeAnnotationLayer: () => {},
-  close: () => {},
-};
+const AUTOMATIC_COLOR = '';
 
 const NotFoundContentWrapper = styled.div`
   && > div:first-child {
@@ -134,8 +159,34 @@ const NotFoundContent = () => (
   </NotFoundContentWrapper>
 );
 
-class AnnotationLayer extends PureComponent {
-  constructor(props) {
+class AnnotationLayer extends PureComponent<
+  AnnotationLayerProps,
+  AnnotationLayerState
+> {
+  static defaultProps = {
+    name: '',
+    annotationType: DEFAULT_ANNOTATION_TYPE,
+    sourceType: '',
+    color: AUTOMATIC_COLOR,
+    opacity: '',
+    style: 'solid',
+    width: 1,
+    showMarkers: false,
+    hideLine: false,
+    overrides: {},
+    colorScheme: 'd3Category10',
+    show: true,
+    showLabel: false,
+    titleColumn: '',
+    descriptionColumns: [],
+    timeColumn: '',
+    intervalEndColumn: '',
+    addAnnotationLayer: () => {},
+    removeAnnotationLayer: () => {},
+    close: () => {},
+  };
+
+  constructor(props: AnnotationLayerProps) {
     super(props);
     const {
       name,
@@ -159,42 +210,46 @@ class AnnotationLayer extends PureComponent {
     } = props;
 
     // Only allow override whole time_range
-    if ('since' in overrides || 'until' in overrides) {
-      overrides.time_range = null;
-      delete overrides.since;
-      delete overrides.until;
+    const processedOverrides: AnnotationOverrides = overrides
+      ? { ...overrides }
+      : {};
+    if ('since' in processedOverrides || 'until' in processedOverrides) {
+      processedOverrides.time_range = null;
+      delete processedOverrides.since;
+      delete processedOverrides.until;
     }
 
     // Check if annotationType is supported by this chart
-    const metadata = getChartMetadataRegistry().get(vizType);
+    const metadata = vizType ? getChartMetadataRegistry().get(vizType) : null;
     const supportedAnnotationTypes = metadata?.supportedAnnotationTypes || [];
+    const resolvedAnnotationType = annotationType || DEFAULT_ANNOTATION_TYPE;
     const validAnnotationType = supportedAnnotationTypes.includes(
-      annotationType,
+      resolvedAnnotationType,
     )
-      ? annotationType
+      ? resolvedAnnotationType
       : supportedAnnotationTypes[0];
 
     this.state = {
       // base
-      name,
-      annotationType: validAnnotationType,
-      sourceType,
-      value,
-      overrides,
-      show,
-      showLabel,
+      name: name || '',
+      annotationType: validAnnotationType || DEFAULT_ANNOTATION_TYPE,
+      sourceType: sourceType || null,
+      value: value || null,
+      overrides: processedOverrides,
+      show: show ?? true,
+      showLabel: showLabel ?? false,
       // slice
-      titleColumn,
-      descriptionColumns,
-      timeColumn,
-      intervalEndColumn,
+      titleColumn: titleColumn || '',
+      descriptionColumns: descriptionColumns || [],
+      timeColumn: timeColumn || '',
+      intervalEndColumn: intervalEndColumn || '',
       // display
       color: color || AUTOMATIC_COLOR,
-      opacity,
-      style,
-      width,
-      showMarkers,
-      hideLine,
+      opacity: opacity || '',
+      style: style || 'solid',
+      width: width ?? 1,
+      showMarkers: showMarkers ?? false,
+      hideLine: hideLine ?? false,
       // refData
       isNew: !name,
       slice: null,
@@ -229,57 +284,71 @@ class AnnotationLayer extends PureComponent {
       /* The value prop is the id of the chart/native. This function will set
       value in state to an object with the id as value.value to be used by
       AsyncSelect */
-      this.fetchAppliedAnnotation(value);
+      if (value !== null && typeof value !== 'object') {
+        this.fetchAppliedAnnotation(value);
+      }
     }
   }
 
-  componentDidUpdate(prevProps, prevState) {
+  componentDidUpdate(
+    _prevProps: AnnotationLayerProps,
+    prevState: AnnotationLayerState,
+  ): void {
     if (this.shouldFetchSliceData(prevState)) {
       const { value } = this.state;
-      this.fetchSliceData(value.value);
+      if (value && typeof value === 'object' && 'value' in value) {
+        this.fetchSliceData(value.value);
+      }
     }
   }
 
-  getSupportedSourceTypes(annotationType) {
+  getSupportedSourceTypes(annotationType: string): SelectOption[] {
     // Get vis types that can be source.
     const sources = getChartMetadataRegistry()
       .entries()
       .filter(({ value: chartMetadata }) =>
-        chartMetadata.canBeAnnotationType(annotationType),
+        chartMetadata?.canBeAnnotationType(annotationType),
       )
       .map(({ key, value: chartMetadata }) => ({
         value: key === VizType.Line ? 'line' : key,
-        label: chartMetadata.name,
+        label: chartMetadata?.name || key,
       }));
     // Prepend native source if applicable
-    if (ANNOTATION_TYPES_METADATA[annotationType]?.supportNativeSource) {
+    const annotationMeta =
+      ANNOTATION_TYPES_METADATA[
+        annotationType as keyof typeof ANNOTATION_TYPES_METADATA
+      ];
+    if (annotationMeta && 'supportNativeSource' in annotationMeta) {
       sources.unshift(ANNOTATION_SOURCE_TYPES_METADATA.NATIVE);
     }
     return sources;
   }
 
-  shouldFetchAppliedAnnotation() {
+  shouldFetchAppliedAnnotation(): boolean {
     const { value, sourceType } = this.state;
-    return value && requiresQuery(sourceType);
+    return !!value && requiresQuery(sourceType ?? undefined);
   }
 
-  shouldFetchSliceData(prevState) {
+  shouldFetchSliceData(prevState: AnnotationLayerState): boolean {
     const { value, sourceType } = this.state;
     const isChart =
       sourceType !== ANNOTATION_SOURCE_TYPES.NATIVE &&
-      requiresQuery(sourceType);
+      requiresQuery(sourceType ?? undefined);
     const valueIsNew = value && prevState.value !== value;
-    return valueIsNew && isChart;
+    return !!valueIsNew && isChart;
   }
 
-  isValidFormulaAnnotation(expression, annotationType) {
+  isValidFormulaAnnotation(
+    expression: string | number | SelectOption | null,
+    annotationType: string,
+  ): boolean {
     if (annotationType === ANNOTATION_TYPES.FORMULA) {
-      return isValidExpression(expression);
+      return isValidExpression(expression as string);
     }
     return true;
   }
 
-  isValidForm() {
+  isValidForm(): boolean {
     const {
       name,
       annotationType,
@@ -302,11 +371,13 @@ class AnnotationLayer extends PureComponent {
         errors.push(validateNonEmpty(intervalEndColumn));
       }
     }
-    errors.push(!this.isValidFormulaAnnotation(value, annotationType));
+    if (!this.isValidFormulaAnnotation(value, annotationType)) {
+      errors.push(t('Invalid formula expression'));
+    }
     return !errors.filter(x => x).length;
   }
 
-  handleAnnotationType(annotationType) {
+  handleAnnotationType(annotationType: string): void {
     this.setState({
       annotationType,
       sourceType: null,
@@ -315,7 +386,7 @@ class AnnotationLayer extends PureComponent {
     });
   }
 
-  handleAnnotationSourceType(sourceType) {
+  handleAnnotationSourceType(sourceType: string): void {
     const { sourceType: prevSourceType } = this.state;
 
     if (prevSourceType !== sourceType) {
@@ -327,24 +398,28 @@ class AnnotationLayer extends PureComponent {
     }
   }
 
-  handleSelectValue(selectedValueObject) {
+  handleSelectValue(selectedValueObject: SelectOption): void {
     this.setState({
       value: selectedValueObject,
       descriptionColumns: [],
-      intervalEndColumn: null,
-      timeColumn: null,
-      titleColumn: null,
+      intervalEndColumn: '',
+      timeColumn: '',
+      titleColumn: '',
       overrides: { time_range: null },
     });
   }
 
-  handleTextValue(inputValue) {
+  handleTextValue(inputValue: string): void {
     this.setState({
       value: inputValue,
     });
   }
 
-  fetchNativeAnnotations = async (search, page, pageSize) => {
+  fetchNativeAnnotations = async (
+    search: string,
+    page: number,
+    pageSize: number,
+  ): Promise<{ data: SelectOption[]; totalCount: number }> => {
     const queryParams = rison.encode({
       filters: [
         {
@@ -364,7 +439,7 @@ class AnnotationLayer extends PureComponent {
 
     const { result, count } = json;
 
-    const layersArray = result.map(layer => ({
+    const layersArray = result.map((layer: { id: number; name: string }) => ({
       value: layer.id,
       label: layer.name,
     }));
@@ -375,7 +450,11 @@ class AnnotationLayer extends PureComponent {
     };
   };
 
-  fetchCharts = async (search, page, pageSize) => {
+  fetchCharts = async (
+    search: string,
+    page: number,
+    pageSize: number,
+  ): Promise<{ data: SelectOption[]; totalCount: number }> => {
     const { annotationType } = this.state;
 
     const queryParams = rison.encode({
@@ -401,11 +480,11 @@ class AnnotationLayer extends PureComponent {
     const registry = getChartMetadataRegistry();
 
     const chartsArray = result
-      .filter(chart => {
+      .filter((chart: { id: number; slice_name: string; viz_type: string }) => {
         const metadata = registry.get(chart.viz_type);
         return metadata && metadata.canBeAnnotationType(annotationType);
       })
-      .map(chart => ({
+      .map((chart: { id: number; slice_name: string; viz_type: string }) => ({
         value: chart.id,
         label: chart.slice_name,
         viz_type: chart.viz_type,
@@ -417,7 +496,11 @@ class AnnotationLayer extends PureComponent {
     };
   };
 
-  fetchOptions = (search, page, pageSize) => {
+  fetchOptions = (
+    search: string,
+    page: number,
+    pageSize: number,
+  ): Promise<{ data: SelectOption[]; totalCount: number }> => {
     const { sourceType } = this.state;
 
     if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
@@ -426,7 +509,7 @@ class AnnotationLayer extends PureComponent {
     return this.fetchCharts(search, page, pageSize);
   };
 
-  fetchSliceData = id => {
+  fetchSliceData = (id: string | number): void => {
     const queryParams = rison.encode({
       columns: ['query_context'],
     });
@@ -439,7 +522,9 @@ class AnnotationLayer extends PureComponent {
       const dataObject = {
         data: {
           ...formData,
-          groupby: formData.groupby?.map(column => getColumnLabel(column)),
+          groupby: formData.groupby?.map((column: QueryFormColumn) =>
+            getColumnLabel(column),
+          ),
         },
       };
       this.setState({
@@ -448,7 +533,7 @@ class AnnotationLayer extends PureComponent {
     });
   };
 
-  fetchAppliedChart(id) {
+  fetchAppliedChart(id: string | number): void {
     const { annotationType } = this.state;
     const registry = getChartMetadataRegistry();
     const queryParams = rison.encode({
@@ -474,7 +559,9 @@ class AnnotationLayer extends PureComponent {
           slice: {
             data: {
               ...formData,
-              groupby: formData.groupby?.map(column => getColumnLabel(column)),
+              groupby: formData.groupby?.map((column: QueryFormColumn) =>
+                getColumnLabel(column),
+              ),
             },
           },
         });
@@ -482,7 +569,7 @@ class AnnotationLayer extends PureComponent {
     });
   }
 
-  fetchAppliedNativeAnnotation(id) {
+  fetchAppliedNativeAnnotation(id: string | number): void {
     SupersetClient.get({
       endpoint: `/api/v1/annotation_layer/${id}`,
     }).then(({ json }) => {
@@ -497,7 +584,7 @@ class AnnotationLayer extends PureComponent {
     });
   }
 
-  fetchAppliedAnnotation(id) {
+  fetchAppliedAnnotation(id: string | number): void {
     const { sourceType } = this.state;
 
     if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
@@ -506,12 +593,12 @@ class AnnotationLayer extends PureComponent {
     return this.fetchAppliedChart(id);
   }
 
-  deleteAnnotation() {
-    this.props.removeAnnotationLayer();
-    this.props.close();
+  deleteAnnotation(): void {
+    this.props.removeAnnotationLayer?.();
+    this.props.close?.();
   }
 
-  applyAnnotation() {
+  applyAnnotation(): void {
     const { value, sourceType } = this.state;
     if (this.isValidForm()) {
       const annotationFields = [
@@ -532,32 +619,42 @@ class AnnotationLayer extends PureComponent {
         'timeColumn',
         'intervalEndColumn',
       ];
-      const newAnnotation = {};
+      const newAnnotation: Record<string, unknown> = {};
       annotationFields.forEach(field => {
-        if (this.state[field] !== null) {
-          newAnnotation[field] = this.state[field];
+        const stateValue = this.state[field as keyof AnnotationLayerState];
+        if (stateValue !== null) {
+          newAnnotation[field] = stateValue;
         }
       });
 
       // Prepare newAnnotation.value for use in runAnnotationQuery()
-      const applicableValue = requiresQuery(sourceType) ? value.value : value;
+      const applicableValue =
+        requiresQuery(sourceType ?? undefined) &&
+        value &&
+        typeof value === 'object'
+          ? (value as SelectOption).value
+          : value;
       newAnnotation.value = applicableValue;
 
       if (newAnnotation.color === AUTOMATIC_COLOR) {
         newAnnotation.color = null;
       }
 
-      this.props.addAnnotationLayer(newAnnotation);
+      this.props.addAnnotationLayer?.(newAnnotation);
       this.setState({ isNew: false });
     }
   }
 
-  submitAnnotation() {
+  submitAnnotation(): void {
     this.applyAnnotation();
-    this.props.close();
+    this.props.close?.();
   }
 
-  renderChartHeader(label, description, value) {
+  renderChartHeader(
+    label: string,
+    description: string,
+    value: string | number | SelectOption | null,
+  ): React.ReactNode {
     return (
       <ControlHeader
         hovered
@@ -568,11 +665,11 @@ class AnnotationLayer extends PureComponent {
     );
   }
 
-  renderValueConfiguration() {
+  renderValueConfiguration(): React.ReactNode {
     const { annotationType, sourceType, value } = this.state;
     let label = '';
     let description = '';
-    if (requiresQuery(sourceType)) {
+    if (requiresQuery(sourceType ?? undefined)) {
       if (sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
         label = t('Annotation layer');
         description = t('Select the Annotation Layer you would like to use.');
@@ -592,7 +689,7 @@ class AnnotationLayer extends PureComponent {
         in milliseconds since epoch. mathjs is used to evaluate the formulas.
         Example: '2x+5'`);
     }
-    if (requiresQuery(sourceType)) {
+    if (requiresQuery(sourceType ?? undefined)) {
       return (
         <AsyncSelect
           /* key to force re-render on sourceType change */
@@ -608,6 +705,8 @@ class AnnotationLayer extends PureComponent {
       );
     }
     if (annotationType === ANNOTATION_TYPES.FORMULA) {
+      // Extract primitive value for TextControl (formula is always a string)
+      const textValue = typeof value === 'object' ? null : value;
       return (
         <TextControl
           name="annotation-layer-value"
@@ -616,7 +715,7 @@ class AnnotationLayer extends PureComponent {
           description={description}
           label={label}
           placeholder=""
-          value={value}
+          value={textValue}
           onChange={this.handleTextValue}
           validationErrors={
             !this.isValidFormulaAnnotation(value, annotationType)
@@ -629,7 +728,7 @@ class AnnotationLayer extends PureComponent {
     return '';
   }
 
-  renderSliceConfiguration() {
+  renderSliceConfiguration(): React.ReactNode {
     const {
       annotationType,
       sourceType,
@@ -679,7 +778,9 @@ class AnnotationLayer extends PureComponent {
                 clearable={false}
                 options={timeColumnOptions}
                 value={timeColumn}
-                onChange={v => this.setState({ timeColumn: v })}
+                onChange={(
+                  v: string | number | (string | number)[] | null | undefined,
+                ) => this.setState({ timeColumn: String(v ?? '') })}
               />
             )}
             {annotationType === ANNOTATION_TYPES.INTERVAL && (
@@ -694,7 +795,14 @@ class AnnotationLayer extends PureComponent {
                 validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
                 options={columns}
                 value={intervalEndColumn}
-                onChange={value => this.setState({ intervalEndColumn: value })}
+                onChange={(
+                  value:
+                    | string
+                    | number
+                    | (string | number)[]
+                    | null
+                    | undefined,
+                ) => this.setState({ intervalEndColumn: String(value ?? '') })}
               />
             )}
             <SelectControl
@@ -705,7 +813,9 @@ class AnnotationLayer extends PureComponent {
               description={t('Pick a title for you annotation.')}
               options={[{ value: '', label: t('None') }].concat(columns)}
               value={titleColumn}
-              onChange={value => this.setState({ titleColumn: value })}
+              onChange={(
+                value: string | number | (string | number)[] | null | undefined,
+              ) => this.setState({ titleColumn: String(value ?? '') })}
             />
             {annotationType !== ANNOTATION_TYPES.TIME_SERIES && (
               <SelectControl
@@ -719,7 +829,17 @@ class AnnotationLayer extends PureComponent {
                 multi
                 options={columns}
                 value={descriptionColumns}
-                onChange={value => this.setState({ descriptionColumns: value })}
+                onChange={(
+                  value:
+                    | string
+                    | number
+                    | (string | number)[]
+                    | null
+                    | undefined,
+                ) => {
+                  const cols = Array.isArray(value) ? value.map(String) : [];
+                  this.setState({ descriptionColumns: cols });
+                }}
               />
             )}
             <div style={{ marginTop: '1rem' }}>
@@ -784,7 +904,7 @@ class AnnotationLayer extends PureComponent {
     return '';
   }
 
-  renderDisplayConfiguration() {
+  renderDisplayConfiguration(): React.ReactNode {
     const {
       color,
       opacity,
@@ -794,9 +914,10 @@ class AnnotationLayer extends PureComponent {
       hideLine,
       annotationType,
     } = this.state;
-    const colorScheme = getCategoricalSchemeRegistry()
-      .get(this.props.colorScheme)
-      .colors.concat();
+    const colorScheme =
+      getCategoricalSchemeRegistry()
+        .get(this.props.colorScheme)
+        ?.colors.concat() ?? [];
     if (
       color &&
       color !== AUTOMATIC_COLOR &&
@@ -823,7 +944,9 @@ class AnnotationLayer extends PureComponent {
           ]}
           value={style}
           clearable={false}
-          onChange={v => this.setState({ style: v })}
+          onChange={(
+            v: string | number | (string | number)[] | null | undefined,
+          ) => this.setState({ style: String(v ?? 'solid') })}
         />
         <SelectControl
           ariaLabel={t('Annotation layer opacity')}
@@ -837,7 +960,9 @@ class AnnotationLayer extends PureComponent {
             { value: 'opacityHigh', label: '0.8' },
           ]}
           value={opacity}
-          onChange={value => this.setState({ opacity: value })}
+          onChange={(
+            value: string | number | (string | number)[] | null | undefined,
+          ) => this.setState({ opacity: String(value ?? '') })}
         />
         <div
           style={{
@@ -905,14 +1030,19 @@ class AnnotationLayer extends PureComponent {
     );
   }
 
-  render() {
+  render(): React.ReactNode {
     const { isNew, name, annotationType, sourceType, show, showLabel } =
       this.state;
     const isValid = this.isValidForm();
-    const metadata = getChartMetadataRegistry().get(this.props.vizType);
+    const metadata = this.props.vizType
+      ? getChartMetadataRegistry().get(this.props.vizType)
+      : null;
     const supportedAnnotationTypes = metadata
       ? metadata.supportedAnnotationTypes.map(
-          type => ANNOTATION_TYPES_METADATA[type],
+          type =>
+            ANNOTATION_TYPES_METADATA[
+              type as keyof typeof ANNOTATION_TYPES_METADATA
+            ],
         )
       : [];
     const supportedSourceTypes = this.getSupportedSourceTypes(annotationType);
@@ -989,7 +1119,7 @@ class AnnotationLayer extends PureComponent {
             <Button
               buttonSize="small"
               buttonStyle="secondary"
-              onClick={() => this.props.close()}
+              onClick={() => this.props.close?.()}
             >
               {t('Cancel')}
             </Button>
@@ -1026,7 +1156,4 @@ class AnnotationLayer extends PureComponent {
   }
 }
 
-AnnotationLayer.propTypes = propTypes;
-AnnotationLayer.defaultProps = defaultProps;
-
 export default withTheme(AnnotationLayer);
diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts
similarity index 69%
rename from superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js
rename to superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts
index cdf7a161f7..b29cde8b6e 100644
--- a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js
+++ b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts
@@ -18,12 +18,25 @@
  */
 import { t } from '@superset-ui/core';
 
-function extractTypes(metadata) {
-  return Object.keys(metadata).reduce((prev, key) => {
-    const result = prev;
-    result[key] = key;
-    return result;
-  }, {});
+interface Annotation {
+  sourceType?: string;
+  timeColumn?: string;
+  intervalEndColumn?: string;
+  titleColumn?: string;
+  descriptionColumns?: string[];
+}
+
+function extractTypes<T extends Record<string, { value: string }>>(
+  metadata: T,
+): Record<keyof T, string> {
+  return Object.keys(metadata).reduce(
+    (prev, key) => {
+      const result = prev;
+      result[key as keyof T] = key;
+      return result;
+    },
+    {} as Record<keyof T, string>,
+  );
 }
 
 export const ANNOTATION_TYPES_METADATA = {
@@ -62,7 +75,9 @@ export const ANNOTATION_SOURCE_TYPES = extractTypes(
   ANNOTATION_SOURCE_TYPES_METADATA,
 );
 
-export function requiresQuery(annotationSourceType) {
+export function requiresQuery(
+  annotationSourceType: string | undefined,
+): boolean {
   return !!annotationSourceType;
 }
 
@@ -71,11 +86,16 @@ const NATIVE_COLUMN_NAMES = {
   intervalEndColumn: 'end_dttm',
   titleColumn: 'short_descr',
   descriptionColumns: ['long_descr'],
-};
+} as const;
 
-export function applyNativeColumns(annotation) {
+export function applyNativeColumns(annotation: Annotation): Annotation {
   if (annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) {
-    return { ...annotation, ...NATIVE_COLUMN_NAMES };
+    return {
+      ...annotation,
+      ...NATIVE_COLUMN_NAMES,
+      // Spread to convert readonly array to mutable
+      descriptionColumns: [...NATIVE_COLUMN_NAMES.descriptionColumns],
+    };
   }
   return annotation;
 }
diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.test.jsx b/superset-frontend/src/explore/components/controls/BoundsControl.test.tsx
similarity index 100%
rename from superset-frontend/src/explore/components/controls/BoundsControl.test.jsx
rename to superset-frontend/src/explore/components/controls/BoundsControl.test.tsx
diff --git a/superset-frontend/src/explore/components/controls/CheckboxControl.jsx b/superset-frontend/src/explore/components/controls/CheckboxControl.tsx
similarity index 68%
rename from superset-frontend/src/explore/components/controls/CheckboxControl.jsx
rename to superset-frontend/src/explore/components/controls/CheckboxControl.tsx
index dd551dd996..f9120159e9 100644
--- a/superset-frontend/src/explore/components/controls/CheckboxControl.jsx
+++ b/superset-frontend/src/explore/components/controls/CheckboxControl.tsx
@@ -16,22 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Component } from 'react';
-import PropTypes from 'prop-types';
+import { Component, type ReactNode } from 'react';
 import { styled, css } from '@apache-superset/core/ui';
 import { Checkbox } from '@superset-ui/core/components';
 import ControlHeader from '../ControlHeader';
 
-const propTypes = {
-  value: PropTypes.bool,
-  label: PropTypes.string,
-  onChange: PropTypes.func,
-};
-
-const defaultProps = {
-  value: false,
-  onChange: () => {},
-};
+interface CheckboxControlProps {
+  value?: boolean;
+  label?: ReactNode;
+  name?: string;
+  description?: ReactNode;
+  hovered?: boolean;
+  onChange?: (value: boolean) => void;
+  validationErrors?: string[];
+  placeholder?: string;
+  debounceDelay?: number;
+}
 
 const CheckBoxControlWrapper = styled.div`
   ${({ theme }) => css`
@@ -47,28 +47,28 @@ const CheckBoxControlWrapper = styled.div`
   `}
 `;
 
-export default class CheckboxControl extends Component {
-  onChange() {
-    this.props.onChange(!this.props.value);
-  }
+export default class CheckboxControl extends Component<CheckboxControlProps> {
+  static defaultProps = {
+    value: false,
+    onChange: () => {},
+  };
+
+  onChange = (): void => {
+    this.props.onChange?.(!this.props.value);
+  };
 
-  renderCheckbox() {
-    return (
-      <Checkbox
-        onChange={this.onChange.bind(this)}
-        checked={!!this.props.value}
-      />
-    );
+  renderCheckbox(): ReactNode {
+    return <Checkbox onChange={this.onChange} checked={!!this.props.value} />;
   }
 
-  render() {
+  render(): ReactNode {
     if (this.props.label) {
       return (
         <CheckBoxControlWrapper>
           <ControlHeader
             {...this.props}
             leftNode={this.renderCheckbox()}
-            onClick={this.onChange.bind(this)}
+            onClick={this.onChange}
           />
         </CheckBoxControlWrapper>
       );
@@ -76,5 +76,3 @@ export default class CheckboxControl extends Component {
     return this.renderCheckbox();
   }
 }
-CheckboxControl.propTypes = propTypes;
-CheckboxControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx b/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
similarity index 67%
rename from superset-frontend/src/explore/components/controls/CollectionControl/index.jsx
rename to superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
index 04d91a8dde..cf86cffd64 100644
--- a/superset-frontend/src/explore/components/controls/CollectionControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Component } from 'react';
+import React, { Component } from 'react';
 import PropTypes from 'prop-types';
 import { IconTooltip, List } from '@superset-ui/core/components';
 import { nanoid } from 'nanoid';
 import { t } from '@superset-ui/core';
-import { withTheme } from '@apache-superset/core/ui';
+import { withTheme, type SupersetTheme } from '@apache-superset/core/ui';
 import {
   SortableContainer,
   SortableHandle,
@@ -37,6 +37,27 @@ import ControlHeader from 'src/explore/components/ControlHeader';
 import CustomListItem from 'src/explore/components/controls/CustomListItem';
 import controlMap from '..';
 
+interface CollectionItem {
+  key?: string;
+  [key: string]: unknown;
+}
+
+interface CollectionControlProps {
+  name: string;
+  label?: string | null;
+  description?: string | null;
+  placeholder?: string;
+  addTooltip?: string;
+  itemGenerator?: () => CollectionItem;
+  keyAccessor?: (item: CollectionItem) => string;
+  onChange?: (value: CollectionItem[]) => void;
+  value?: CollectionItem[];
+  isFloat?: boolean;
+  isInt?: boolean;
+  controlName: string;
+  theme: SupersetTheme;
+}
+
 const propTypes = {
   name: PropTypes.string.isRequired,
   label: PropTypes.string,
@@ -52,13 +73,13 @@ const propTypes = {
   controlName: PropTypes.string.isRequired,
 };
 
-const defaultProps = {
+const defaultProps: Partial<CollectionControlProps> = {
   label: null,
   description: null,
   onChange: () => {},
   placeholder: t('Empty collection'),
   itemGenerator: () => ({ key: nanoid(11) }),
-  keyAccessor: o => o.key,
+  keyAccessor: (o: CollectionItem) => o.key ?? '',
   value: [],
   addTooltip: t('Add an item'),
 };
@@ -73,63 +94,81 @@ const SortableDragger = SortableHandle(() => (
   />
 ));
 
-class CollectionControl extends Component {
-  constructor(props) {
+class CollectionControl extends Component<CollectionControlProps> {
+  static propTypes = propTypes;
+
+  static defaultProps = defaultProps;
+
+  constructor(props: CollectionControlProps) {
     super(props);
     this.onAdd = this.onAdd.bind(this);
   }
 
-  onChange(i, value) {
-    const newValue = [...this.props.value];
-    newValue[i] = { ...this.props.value[i], ...value };
-    this.props.onChange(newValue);
+  onChange(i: number, value: CollectionItem) {
+    const currentValue = this.props.value ?? [];
+    const newValue = [...currentValue];
+    newValue[i] = { ...currentValue[i], ...value };
+    this.props.onChange?.(newValue);
   }
 
   onAdd() {
-    this.props.onChange(this.props.value.concat([this.props.itemGenerator()]));
+    const currentValue = this.props.value ?? [];
+    const newItem = this.props.itemGenerator?.();
+    // Cast needed: original JS allowed undefined items from itemGenerator
+    this.props.onChange?.(
+      currentValue.concat([newItem] as unknown as CollectionItem[]),
+    );
   }
 
-  onSortEnd({ oldIndex, newIndex }) {
-    this.props.onChange(arrayMove(this.props.value, oldIndex, newIndex));
+  onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) {
+    const currentValue = this.props.value ?? [];
+    this.props.onChange?.(arrayMove(currentValue, oldIndex, newIndex));
   }
 
-  removeItem(i) {
-    this.props.onChange(this.props.value.filter((o, ix) => i !== ix));
+  removeItem(i: number) {
+    const currentValue = this.props.value ?? [];
+    this.props.onChange?.(currentValue.filter((o, ix) => i !== ix));
   }
 
   renderList() {
-    if (this.props.value.length === 0) {
+    const currentValue = this.props.value ?? [];
+    if (currentValue.length === 0) {
       return <div className="text-muted">{this.props.placeholder}</div>;
     }
-    const Control = controlMap[this.props.controlName];
+    const Control = (controlMap as Record<string, React.ComponentType<any>>)[
+      this.props.controlName
+    ];
+    const keyAccessor =
+      this.props.keyAccessor ?? ((o: CollectionItem) => o.key ?? '');
     return (
       <SortableList
         useDragHandle
         lockAxis="y"
         onSortEnd={this.onSortEnd.bind(this)}
         bordered
-        css={theme => ({
+        css={(theme: SupersetTheme) => ({
           borderRadius: theme.borderRadius,
         })}
       >
-        {this.props.value.map((o, i) => {
+        {currentValue.map((o: CollectionItem, i: number) => {
           // label relevant only for header, not here
-          const { label, ...commonProps } = this.props;
+          const { label, theme, ...commonProps } = this.props;
           return (
             <SortableListItem
+              selectable={false}
               className="clearfix"
-              css={theme => ({
+              css={(theme: SupersetTheme) => ({
                 alignItems: 'center',
                 justifyContent: 'flex-start',
                 display: 'flex',
                 paddingInline: theme.sizeUnit * 6,
               })}
-              key={this.props.keyAccessor(o)}
+              key={keyAccessor(o)}
               index={i}
             >
               <SortableDragger />
               <div
-                css={theme => ({
+                css={(theme: SupersetTheme) => ({
                   flex: 1,
                   marginLeft: theme.sizeUnit * 2,
                   marginRight: theme.sizeUnit * 2,
@@ -148,7 +187,7 @@ class CollectionControl extends Component {
                 tooltip={t('Remove item')}
                 mouseEnterDelay={0}
                 mouseLeaveDelay={0}
-                css={theme => ({
+                css={(theme: SupersetTheme) => ({
                   padding: 0,
                   minWidth: 'auto',
                   height: 'auto',
@@ -190,7 +229,4 @@ class CollectionControl extends Component {
   }
 }
 
-CollectionControl.propTypes = propTypes;
-CollectionControl.defaultProps = defaultProps;
-
 export default withTheme(CollectionControl);
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
index baf72c653a..edfcf62c3b 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 
+import type React from 'react';
 import { Route } from 'react-router-dom';
 import fetchMock from 'fetch-mock';
 import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core';
@@ -28,7 +29,7 @@ import {
   waitFor,
 } from 'spec/helpers/testing-library';
 import { fallbackExploreInitialData } from 'src/explore/fixtures';
-import type { DatasetObject, ColumnObject } from 'src/features/datasets/types';
+import type { ColumnObject } from 'src/features/datasets/types';
 import DatasourceControl from '.';
 
 const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
@@ -46,20 +47,35 @@ afterEach(() => {
   jest.clearAllMocks(); // Clears mock history but keeps spy in place
 });
 
-type TestDatasource = Omit<
-  Partial<DatasetObject>,
-  'columns' | 'main_dttm_col'
-> & {
+interface TestDatasource {
+  id?: number;
   name: string;
-  database: { name: string };
+  datasource_name?: string;
+  database: {
+    id: number;
+    database_name: string;
+    name?: string;
+    backend?: string;
+  };
   columns?: Partial<ColumnObject>[];
   type?: DatasourceType;
   main_dttm_col?: string | null;
-};
+  owners?: Array<{
+    first_name: string;
+    last_name: string;
+    id: number;
+    username?: string;
+  }>;
+  sql?: string;
+  metrics?: Array<{ id: number; metric_name: string }>;
+  [key: string]: unknown;
+}
 
 const mockDatasource: TestDatasource = {
   id: 25,
   database: {
+    id: 1,
+    database_name: 'examples',
     name: 'examples',
   },
   name: 'channels',
@@ -69,39 +85,50 @@ const mockDatasource: TestDatasource = {
   owners: [{ first_name: 'john', last_name: 'doe', id: 1, username: 'jd' }],
   sql: 'SELECT * FROM mock_datasource_sql',
 };
-const createProps = (overrides: JsonObject = {}) => ({
-  hovered: false,
-  type: 'DatasourceControl',
-  label: 'Datasource',
-  default: null,
-  description: null,
-  value: '25__table',
-  form_data: {},
-  datasource: mockDatasource,
-  validationErrors: [],
-  name: 'datasource',
-  actions: {
-    changeDatasource: jest.fn(),
-    setControlValue: jest.fn(),
-  },
-  isEditable: true,
-  user: {
-    createdOn: '2021-04-27T18:12:38.952304',
-    email: 'admin',
-    firstName: 'admin',
-    isActive: true,
-    lastName: 'admin',
-    permissions: {},
-    roles: { Admin: Array(173) },
-    userId: 1,
-    username: 'admin',
-  },
-  onChange: jest.fn(),
-  onDatasourceSave: jest.fn(),
-  ...overrides,
-});
 
-async function openAndSaveChanges(datasource: TestDatasource) {
+// Use type assertion for test props since the component is wrapped with withTheme
+// The withTheme HOC makes the props type complex, so we cast through unknown to bypass type check
+type DatasourceControlComponentProps = React.ComponentProps<
+  typeof DatasourceControl
+>;
+const createProps = (
+  overrides: JsonObject = {},
+): DatasourceControlComponentProps =>
+  ({
+    hovered: false,
+    type: 'DatasourceControl',
+    label: 'Datasource',
+    default: null,
+    description: null,
+    value: '25__table',
+    form_data: {},
+    datasource: mockDatasource,
+    validationErrors: [],
+    name: 'datasource',
+    actions: {
+      changeDatasource: jest.fn(),
+      setControlValue: jest.fn(),
+    },
+    isEditable: true,
+    user: {
+      createdOn: '2021-04-27T18:12:38.952304',
+      email: 'admin',
+      firstName: 'admin',
+      isActive: true,
+      lastName: 'admin',
+      permissions: {},
+      roles: { Admin: Array(173) },
+      userId: 1,
+      username: 'admin',
+    },
+    onChange: jest.fn(),
+    onDatasourceSave: jest.fn(),
+    ...overrides,
+  }) as unknown as DatasourceControlComponentProps;
+
+async function openAndSaveChanges(
+  datasource: TestDatasource | Record<string, unknown>,
+) {
   fetchMock.get(
     'glob:*/api/v1/database/?q=*',
     { result: [] },
@@ -259,7 +286,6 @@ test('Click on Edit dataset', async () => {
 
 test('Edit dataset should be disabled when user is not admin', async () => {
   const props = createProps();
-  // @ts-expect-error
   props.user.roles = {};
   props.datasource.owners = [];
   SupersetClientGet.mockImplementationOnce(
@@ -458,11 +484,11 @@ test('should not set the temporal column', async () => {
   const overrideProps = {
     ...props,
     form_data: {
-      granularity_sqla: null,
+      granularity_sqla: undefined,
     },
     datasource: {
       ...props.datasource,
-      main_dttm_col: null,
+      main_dttm_col: undefined,
       columns: [
         {
           column_name: 'test-col',
diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx
similarity index 80%
rename from superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
rename to superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx
index ed582b088e..6c611f69d6 100644
--- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx
@@ -18,10 +18,20 @@
  * under the License.
  */
 
-import { PureComponent } from 'react';
+import React, { PureComponent } from 'react';
 import PropTypes from 'prop-types';
-import { DatasourceType, SupersetClient, t } from '@superset-ui/core';
-import { css, styled, withTheme } from '@apache-superset/core/ui';
+import {
+  DatasourceType,
+  SupersetClient,
+  t,
+  Datasource,
+} from '@superset-ui/core';
+import {
+  css,
+  styled,
+  withTheme,
+  type SupersetTheme,
+} from '@apache-superset/core/ui';
 import { getTemporalColumns } from '@superset-ui/chart-controls';
 import { getUrlParam } from 'src/utils/urlUtils';
 import {
@@ -51,6 +61,68 @@ import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
 import { safeStringify } from 'src/utils/safeStringify';
 import { Link } from 'react-router-dom';
 
+// Extended Datasource interface with all properties used in this component
+interface ExtendedDatasource extends Datasource {
+  sql?: string;
+  select_star?: string;
+  owners?: Array<{
+    id: number;
+    first_name: string;
+    last_name: string;
+    value?: number;
+  }>;
+  extra?: string;
+  health_check_message?: string;
+  database?: {
+    id: number;
+    database_name: string;
+    backend?: string;
+  };
+}
+
+interface User {
+  userId?: number;
+  username?: string;
+  roles?: Record<string, unknown[]>;
+}
+
+interface DatasourceControlActions {
+  changeDatasource: (datasource: ExtendedDatasource) => void;
+  setControlValue: (name: string, value: unknown) => void;
+}
+
+interface FormData {
+  granularity_sqla?: string;
+  [key: string]: unknown;
+}
+
+interface DatasourceControlProps {
+  actions: DatasourceControlActions;
+  onChange?: () => void;
+  value?: string | null;
+  datasource: ExtendedDatasource;
+  form_data?: FormData;
+  isEditable?: boolean;
+  onDatasourceSave?: ((datasource: ExtendedDatasource) => void) | null;
+  theme: SupersetTheme;
+  user: User;
+  // ControlHeader-related props
+  hovered?: boolean;
+  type?: string;
+  label?: string;
+  default?: unknown;
+  description?: string | null;
+  validationErrors?: string[];
+  name?: string;
+}
+
+interface DatasourceControlState {
+  showEditDatasourceModal: boolean;
+  showChangeDatasourceModal: boolean;
+  showSaveDatasetModal: boolean;
+  showDatasource?: boolean;
+}
+
 const propTypes = {
   actions: PropTypes.object.isRequired,
   onChange: PropTypes.func,
@@ -59,6 +131,15 @@ const propTypes = {
   form_data: PropTypes.object.isRequired,
   isEditable: PropTypes.bool,
   onDatasourceSave: PropTypes.func,
+  user: PropTypes.object.isRequired,
+  // ControlHeader-related props
+  hovered: PropTypes.bool,
+  type: PropTypes.string,
+  label: PropTypes.string,
+  default: PropTypes.any,
+  description: PropTypes.string,
+  validationErrors: PropTypes.array,
+  name: PropTypes.string,
 };
 
 const defaultProps = {
@@ -68,7 +149,7 @@ const defaultProps = {
   isEditable: true,
 };
 
-const getDatasetType = datasource => {
+const getDatasetType = (datasource: ExtendedDatasource): string => {
   if (datasource.type === 'query') {
     return 'query';
   }
@@ -139,15 +220,18 @@ const SAVE_AS_DATASET = 'save_as_dataset';
 const VISIBLE_TITLE_LENGTH = 25;
 
 // Assign icon for each DatasourceType.  If no icon assignment is found in the lookup, no icon will render
-export const datasourceIconLookup = {
+export const datasourceIconLookup: Record<string, React.ReactNode> = {
   query: <Icons.ConsoleSqlOutlined className="datasource-svg" />,
   physical_dataset: <Icons.TableOutlined className="datasource-svg" />,
   virtual_dataset: <Icons.ConsoleSqlOutlined className="datasource-svg" />,
 };
 
 // Render title for datasource with tooltip only if text is longer than VISIBLE_TITLE_LENGTH
-export const renderDatasourceTitle = (displayString, tooltip) =>
-  displayString?.length > VISIBLE_TITLE_LENGTH ? (
+export const renderDatasourceTitle = (
+  displayString: string | undefined,
+  tooltip: string,
+) =>
+  displayString?.length && displayString.length > VISIBLE_TITLE_LENGTH ? (
     // Add a tooltip only for long names that will be visually truncated
     <Tooltip title={tooltip}>
       <span className="title-select">{displayString}</span>
@@ -159,12 +243,14 @@ export const renderDatasourceTitle = (displayString, tooltip) =>
   );
 
 // Different data source types use different attributes for the display title
-export const getDatasourceTitle = datasource => {
-  if (datasource?.type === 'query') return datasource?.sql;
+export const getDatasourceTitle = (
+  datasource: ExtendedDatasource | null | undefined,
+): string => {
+  if (datasource?.type === 'query') return datasource?.sql || '';
   return datasource?.name || '';
 };
 
-const preventRouterLinkWhileMetaClicked = evt => {
+const preventRouterLinkWhileMetaClicked = (evt: React.MouseEvent) => {
   if (evt.metaKey) {
     evt.preventDefault();
   } else {
@@ -172,8 +258,15 @@ const preventRouterLinkWhileMetaClicked = evt => {
   }
 };
 
-class DatasourceControl extends PureComponent {
-  constructor(props) {
+class DatasourceControl extends PureComponent<
+  DatasourceControlProps,
+  DatasourceControlState
+> {
+  static propTypes = propTypes;
+
+  static defaultProps = defaultProps;
+
+  constructor(props: DatasourceControlProps) {
     super(props);
     this.state = {
       showEditDatasourceModal: false,
@@ -182,10 +275,13 @@ class DatasourceControl extends PureComponent {
     };
   }
 
-  onDatasourceSave = datasource => {
-    this.props.actions.changeDatasource(datasource);
-    const { temporalColumns, defaultTemporalColumn } =
-      getTemporalColumns(datasource);
+  onDatasourceSave = (datasource: Datasource) => {
+    // Cast to ExtendedDatasource for the component's internal use
+    this.props.actions.changeDatasource(datasource as ExtendedDatasource);
+    // Cast datasource for getTemporalColumns which expects Dataset | QueryResponse
+    const { temporalColumns, defaultTemporalColumn } = getTemporalColumns(
+      datasource as Parameters<typeof getTemporalColumns>[0],
+    );
     const { columns } = datasource;
     // the current granularity_sqla might not be a temporal column anymore
     const timeCol = this.props.form_data?.granularity_sqla;
@@ -238,7 +334,7 @@ class DatasourceControl extends PureComponent {
     }));
   };
 
-  handleMenuItemClick = ({ key }) => {
+  handleMenuItemClick = ({ key }: { key: string }) => {
     switch (key) {
       case CHANGE_DATASET:
         this.toggleChangeDatasourceModal();
@@ -371,12 +467,17 @@ class DatasourceControl extends PureComponent {
             modalBody={
               <ViewQuery
                 sql={datasource?.sql || datasource?.select_star || ''}
+                datasource={`${datasource.id}__${datasource.type}`}
               />
             }
             modalFooter={
               <ViewQueryModalFooter
                 changeDatasource={this.toggleSaveDatasetModal}
-                datasource={datasource}
+                datasource={{
+                  id: String(datasource.id),
+                  sql: datasource.sql || '',
+                  type: datasource.type,
+                }}
               />
             }
             draggable={false}
@@ -406,7 +507,7 @@ class DatasourceControl extends PureComponent {
 
     queryDatasourceMenuItems.push({
       key: SAVE_AS_DATASET,
-      label: t('Save as dataset'),
+      label: <span>{t('Save as dataset')}</span>,
     });
 
     const queryDatasourceMenu = (
@@ -464,8 +565,8 @@ class DatasourceControl extends PureComponent {
         {isMissingDatasource && isMissingParams && (
           <div className="error-alert">
             <ErrorAlert
-              level="warning"
-              errorType={t('Missing URL parameters')}
+              type="warning"
+              message={t('Missing URL parameters')}
               description={t(
                 'The URL is missing the dataset_id or slice_id parameters.',
               )}
@@ -486,7 +587,7 @@ class DatasourceControl extends PureComponent {
             ) : (
               <ErrorAlert
                 type="warning"
-                errorType={t('Missing dataset')}
+                message={t('Missing dataset')}
                 descriptionPre={false}
                 descriptionDetailsCollapsed={false}
                 descriptionDetails={
@@ -498,7 +599,7 @@ class DatasourceControl extends PureComponent {
                     </p>
                     <p>
                       <Button
-                        buttonStyle="warning"
+                        buttonStyle="primary"
                         onClick={() =>
                           this.handleMenuItemClick({ key: CHANGE_DATASET })
                         }
@@ -547,7 +648,9 @@ class DatasourceControl extends PureComponent {
   }
 }
 
-DatasourceControl.propTypes = propTypes;
-DatasourceControl.defaultProps = defaultProps;
-
-export default withTheme(DatasourceControl);
+// withTheme injects the theme prop, so we need to cast the component type
+export default withTheme(
+  DatasourceControl as React.ComponentType<
+    Omit<DatasourceControlProps, 'theme'>
+  >,
+);
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnMetricSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnMetricSelect.tsx
index 0be34889c6..806d66fe81 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnMetricSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnMetricSelect.tsx
@@ -333,7 +333,8 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
           <MetricDefinitionValue
             key={`metric-${idx}`}
             index={idx}
-            option={item}
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            option={item as any}
             onMetricEdit={(changedMetric: Metric | AdhocMetric) => {
               const newValues = [...coercedValue];
               if (changedMetric instanceof AdhocMetric) {
@@ -344,10 +345,14 @@ function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
               onChange(multi ? newValues : newValues[0]);
             }}
             onRemoveMetric={onClickClose}
-            columns={columns}
-            savedMetrics={savedMetrics}
-            savedMetricsOptions={savedMetrics}
-            datasource={datasource}
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            columns={columns as any}
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            savedMetrics={savedMetrics as any}
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            savedMetricsOptions={savedMetrics as any}
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            datasource={datasource as any}
             onMoveLabel={onShiftOptions}
             onDropLabel={() => {}}
             type={`${DndItemType.AdhocMetricOption}_${name}_${label}`}
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx
similarity index 91%
rename from superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx
rename to superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx
index 005bc74e9d..f0cb5b939c 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx
@@ -16,12 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useCallback, useState } from 'react';
+import { ChangeEvent, useCallback, useState } from 'react';
 import { t } from '@superset-ui/core';
 import { styled, useTheme } from '@apache-superset/core/ui';
 import { Input, Tooltip } from '@superset-ui/core/components';
 import { Icons } from '@superset-ui/core/components/Icons';
 
+interface DndColumnSelectPopoverTitleProps {
+  title: string;
+  onChange: (e: ChangeEvent<HTMLInputElement>) => void;
+  isEditDisabled: boolean;
+  hasCustomLabel: boolean;
+}
+
 const StyledInput = styled(Input)`
   border-radius: ${({ theme }) => theme.borderRadius};
   height: 26px;
@@ -34,7 +41,7 @@ export const DndColumnSelectPopoverTitle = ({
   onChange,
   isEditDisabled,
   hasCustomLabel,
-}) => {
+}: DndColumnSelectPopoverTitleProps) => {
   const theme = useTheme();
   const [isHovered, setIsHovered] = useState(false);
   const [isEditMode, setIsEditMode] = useState(false);
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx
index b947f69ff7..21c588a330 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx
@@ -19,7 +19,11 @@
 import thunk from 'redux-thunk';
 import configureStore from 'redux-mock-store';
 
-import { ensureIsArray, QueryFormData } from '@superset-ui/core';
+import {
+  ensureIsArray,
+  QueryFormData,
+  QueryFormMetric,
+} from '@superset-ui/core';
 import { GenericDataType } from '@apache-superset/core/api/core';
 import { ColumnMeta } from '@superset-ui/chart-controls';
 import {
@@ -166,7 +170,7 @@ test('renders options with adhoc metric', async () => {
     setup({
       formData: {
         ...baseFormData,
-        metrics: [adhocMetric],
+        metrics: [adhocMetric as unknown as QueryFormMetric],
       },
     }),
     {
@@ -205,7 +209,7 @@ test('cannot drop a column that is not part of the simple column selection', ()
       {setup({
         formData: {
           ...baseFormData,
-          metrics: [adhocMetric],
+          metrics: [adhocMetric as unknown as QueryFormMetric],
         },
         columns: [{ column_name: 'order_date' }],
       })}
@@ -335,7 +339,7 @@ describe('when disallow_adhoc_metrics is set', () => {
         {setup({
           formData: {
             ...baseFormData,
-            metrics: [adhocMetric],
+            metrics: [adhocMetric as unknown as QueryFormMetric],
           },
           datasource: {
             ...PLACEHOLDER_DATASOURCE,
@@ -383,7 +387,7 @@ describe('when disallow_adhoc_metrics is set', () => {
         {setup({
           formData: {
             ...baseFormData,
-            metrics: [adhocMetric],
+            metrics: [adhocMetric as unknown as QueryFormMetric],
           },
           datasource: {
             ...PLACEHOLDER_DATASOURCE,
diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
index 2f936373cb..6b6b1331e7 100644
--- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
+++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx
@@ -89,10 +89,15 @@ const coerceMetrics = (
         col => col.column_name === metric.column.column_name,
       );
       if (column) {
-        return new AdhocMetric({ ...metric, column });
+        // Cast entire config object to handle type mismatch between @superset-ui/core and local types
+        return new AdhocMetric({
+          ...(metric as unknown as Record<string, unknown>),
+          column,
+        } as Record<string, unknown>);
       }
     }
-    return new AdhocMetric(metric);
+    // Cast to unknown first to handle type mismatch between @superset-ui/core and local AdhocMetric
+    return new AdhocMetric(metric as unknown as Record<string, unknown>);
   });
 };
 
@@ -200,7 +205,11 @@ const DndMetricSelect = (props: any) => {
 
   const onMetricEdit = useCallback(
     (changedMetric: Metric | AdhocMetric, oldMetric: Metric | AdhocMetric) => {
-      if (oldMetric instanceof AdhocMetric && oldMetric.equals(changedMetric)) {
+      if (
+        oldMetric instanceof AdhocMetric &&
+        changedMetric instanceof AdhocMetric &&
+        oldMetric.equals(changedMetric)
+      ) {
         return;
       }
       const newValue = value.map(value => {
@@ -273,7 +282,8 @@ const DndMetricSelect = (props: any) => {
       <MetricDefinitionValue
         key={index}
         index={index}
-        option={option}
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        option={option as any}
         onMetricEdit={onMetricEdit}
         onRemoveMetric={onRemoveMetric}
         columns={props.columns}
@@ -343,9 +353,10 @@ const DndMetricSelect = (props: any) => {
       droppedItem.type === DndItemType.Column
     ) {
       const itemValue = droppedItem.value as ColumnMeta;
-      const config: Partial<AdhocMetric> = {
+      // Cast config to handle ColumnMeta/ColumnType mismatch
+      const config = {
         column: itemValue,
-      };
+      } as Partial<AdhocMetric>;
       if (itemValue.type_generic === GenericDataType.Numeric) {
         config.aggregate = AGGREGATES.SUM;
       } else if (
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.js b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.ts
similarity index 100%
rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.js
rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/AdhocFilter.test.ts
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.ts
similarity index 56%
rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js
rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.ts
index eeea251a39..31c544ffc4 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.ts
@@ -16,10 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { AdhocFilter as CoreAdhocFilter } from '@superset-ui/core';
 import {
   CUSTOM_OPERATORS,
   DISABLE_INPUT_OPERATORS,
   OPERATOR_ENUM_TO_OPERATOR_TYPE,
+  Operators,
 } from 'src/explore/constants';
 import { translateToSql } from '../utils/translateToSQL';
 import { Clauses, ExpressionTypes } from '../types';
@@ -28,15 +30,51 @@ const CUSTOM_OPERATIONS = [...CUSTOM_OPERATORS].map(
   op => OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
 );
 
+interface AdhocFilterInput {
+  expressionType?: string;
+  subject?: string | { column_name?: string; [key: string]: unknown } | null;
+  operator?: string | null;
+  operatorId?: string;
+  comparator?: unknown;
+  clause?: string | null;
+  sqlExpression?: string | null;
+  isExtra?: boolean;
+  isNew?: boolean;
+  datasourceWarning?: boolean;
+  deck_slices?: unknown;
+  layerFilterScope?: unknown;
+  filterOptionName?: string;
+  // Allow additional properties for flexibility
+  [key: string]: unknown;
+}
+
 export default class AdhocFilter {
-  constructor(adhocFilter) {
+  expressionType: string;
+  subject?: string | { column_name?: string; [key: string]: unknown } | null;
+  operator?: string | null;
+  operatorId?: string;
+  comparator?: unknown;
+  clause?: string | null;
+  sqlExpression?: string | null;
+  isExtra: boolean;
+  isNew: boolean;
+  datasourceWarning: boolean;
+  deck_slices?: unknown;
+  layerFilterScope?: unknown;
+  filterOptionName: string;
+
+  constructor(adhocFilter: AdhocFilterInput) {
     this.expressionType = adhocFilter.expressionType || ExpressionTypes.Simple;
     if (this.expressionType === ExpressionTypes.Simple) {
       this.subject = adhocFilter.subject;
       this.operator = adhocFilter.operator?.toUpperCase();
       this.operatorId = adhocFilter.operatorId;
       this.comparator = adhocFilter.comparator;
-      if (DISABLE_INPUT_OPERATORS.indexOf(adhocFilter.operatorId) >= 0) {
+      if (
+        adhocFilter.operatorId &&
+        DISABLE_INPUT_OPERATORS.indexOf(adhocFilter.operatorId as Operators) >=
+          0
+      ) {
         this.comparator = undefined;
       }
       this.clause = adhocFilter.clause || Clauses.Where;
@@ -45,7 +83,9 @@ export default class AdhocFilter {
       this.sqlExpression =
         typeof adhocFilter.sqlExpression === 'string'
           ? adhocFilter.sqlExpression
-          : translateToSql(adhocFilter, { useSimple: true });
+          : translateToSql(adhocFilter as unknown as CoreAdhocFilter, {
+              useSimple: true,
+            });
       this.clause = adhocFilter.clause;
       if (
         adhocFilter.operator &&
@@ -73,16 +113,28 @@ export default class AdhocFilter {
         .substring(2, 15)}`;
   }
 
-  duplicateWith(nextFields) {
-    return new AdhocFilter({
-      ...this,
-      // all duplicated fields are not new (i.e. will not open popup automatically)
-      isNew: false,
+  duplicateWith(nextFields: Partial<AdhocFilterInput>): AdhocFilter {
+    // Spread class properties as plain object for constructor input
+    const currentFields: AdhocFilterInput = {
+      expressionType: this.expressionType,
+      subject: this.subject,
+      operator: this.operator,
+      operatorId: this.operatorId,
+      comparator: this.comparator,
+      clause: this.clause,
+      sqlExpression: this.sqlExpression,
+      isExtra: this.isExtra,
+      isNew: false, // all duplicated fields are not new
+      datasourceWarning: this.datasourceWarning,
+      deck_slices: this.deck_slices,
+      layerFilterScope: this.layerFilterScope,
+      filterOptionName: this.filterOptionName,
       ...nextFields,
-    });
+    };
+    return new AdhocFilter(currentFields);
   }
 
-  equals(adhocFilter) {
+  equals(adhocFilter: AdhocFilter): boolean {
     return (
       adhocFilter.clause === this.clause &&
       adhocFilter.expressionType === this.expressionType &&
@@ -94,10 +146,11 @@ export default class AdhocFilter {
     );
   }
 
-  isValid() {
+  isValid(): boolean {
     if (this.expressionType === ExpressionTypes.Simple) {
       // operators where the comparator is not used
       if (
+        this.operator &&
         DISABLE_INPUT_OPERATORS.map(
           op => OPERATOR_ENUM_TO_OPERATOR_TYPE[op].operation,
         ).indexOf(this.operator) >= 0
@@ -121,16 +174,43 @@ export default class AdhocFilter {
     );
   }
 
-  getDefaultLabel() {
+  getDefaultLabel(): string {
     const label = this.translateToSql();
     return label.length < 43 ? label : `${label.substring(0, 40)}...`;
   }
 
-  getTooltipTitle() {
+  getTooltipTitle(): string {
     return this.translateToSql();
   }
 
-  translateToSql() {
-    return translateToSql(this);
+  translateToSql(): string {
+    return translateToSql(this as unknown as CoreAdhocFilter);
   }
 }
+
+/**
+ * Adapter function to create an AdhocFilter instance from a core AdhocFilter type.
+ * This bridges the type gap between @superset-ui/core's AdhocFilter and the local class.
+ */
+export function fromCoreAdhocFilter(filter: CoreAdhocFilter): AdhocFilter {
+  return new AdhocFilter(filter as AdhocFilterInput);
+}
+
+/**
+ * Type guard to check if an object can be used to construct an AdhocFilter.
+ * Returns true for plain objects that have filter-like properties.
+ */
+export function isDictionaryForAdhocFilter(
+  value: unknown,
+): value is AdhocFilterInput {
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    !(value instanceof AdhocFilter) &&
+    ('expressionType' in value ||
+      'subject' in value ||
+      'operator' in value ||
+      'sqlExpression' in value ||
+      'clause' in value)
+  );
+}
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/AdhocFilterControl.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/AdhocFilterControl.test.tsx
index 7e33db84fa..f7de33068e 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/AdhocFilterControl.test.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/AdhocFilterControl.test.tsx
@@ -22,34 +22,29 @@ import AdhocFilterControl from '.';
 import AdhocFilter from '../AdhocFilter';
 import { Clauses, ExpressionTypes } from '../types';
 
-interface Column {
-  column_name: string;
-  type: string;
-}
-
-interface Database {
-  id: number;
-}
-
-interface Datasource {
-  type: string;
-  database: Database;
-  schema: string;
-  datasource_name: string;
-}
-
-interface Props {
+interface TestProps {
   name: string;
   label: string;
   value: AdhocFilter[];
-  datasource: Datasource;
-  columns: Column[];
+  datasource: {
+    type: string;
+    database: { id: number };
+    schema: string;
+    datasource_name: string;
+    [key: string]: unknown;
+  };
+  columns: Array<{
+    column_name: string;
+    type?: string;
+    [key: string]: unknown;
+  }>;
   onChange: jest.Mock;
   sections: string[];
   operators: string[];
+  [key: string]: unknown;
 }
 
-const createProps = (): Props => ({
+const createProps = (): TestProps => ({
   name: 'filter_control',
   label: 'Filters',
   value: [],
@@ -68,10 +63,16 @@ const createProps = (): Props => ({
   operators: ['==', '>', '<'],
 });
 
-const renderComponent = (props: Partial<Props> = {}) =>
-  render(<AdhocFilterControl {...createProps()} {...props} />, {
-    useDnd: true,
-  });
+const renderComponent = (props: Partial<TestProps> = {}) =>
+  render(
+    <AdhocFilterControl
+      {...(createProps() as Record<string, unknown>)}
+      {...props}
+    />,
+    {
+      useDnd: true,
+    },
+  );
 
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
 describe('AdhocFilterControl', () => {
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx
similarity index 70%
rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx
index 315babb3bf..7ca2a8c491 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx
@@ -16,15 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Component } from 'react';
+import { Component, ReactNode } from 'react';
 import PropTypes from 'prop-types';
 import { t, logging, SupersetClient, ensureIsArray } from '@superset-ui/core';
-import { withTheme } from '@apache-superset/core/ui';
+import { withTheme, type SupersetTheme } from '@apache-superset/core/ui';
 
 import ControlHeader from 'src/explore/components/ControlHeader';
 import adhocMetricType from 'src/explore/components/controls/MetricControl/adhocMetricType';
 import savedMetricType from 'src/explore/components/controls/MetricControl/savedMetricType';
-import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
+import AdhocMetric, {
+  isDictionaryForAdhocMetric,
+} from 'src/explore/components/controls/MetricControl/AdhocMetric';
 import {
   Operators,
   OPERATOR_ENUM_TO_OPERATOR_TYPE,
@@ -39,12 +41,70 @@ import { Icons } from '@superset-ui/core/components/Icons';
 import { Modal } from '@superset-ui/core/components';
 import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
 import AdhocFilterOption from 'src/explore/components/controls/FilterControl/AdhocFilterOption';
-import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
+import AdhocFilter, {
+  isDictionaryForAdhocFilter,
+} from 'src/explore/components/controls/FilterControl/AdhocFilter';
 import adhocFilterType from 'src/explore/components/controls/FilterControl/adhocFilterType';
 import columnType from 'src/explore/components/controls/FilterControl/columnType';
 import { toQueryString } from 'src/utils/urlUtils';
 import { Clauses, ExpressionTypes } from '../types';
 
+interface ColumnMeta {
+  column_name: string;
+  verbose_name?: string;
+  [key: string]: unknown;
+}
+
+interface SavedMetric {
+  metric_name: string;
+  expression: string;
+  [key: string]: unknown;
+}
+
+interface Datasource {
+  id?: number;
+  type?: string;
+  database?: { id: number };
+  datasource_name?: string;
+  catalog?: string;
+  schema?: string;
+  is_sqllab_view?: boolean;
+  [key: string]: unknown;
+}
+
+interface AdhocFilterControlProps {
+  label?: ReactNode;
+  name?: string;
+  sections?: string[];
+  operators?: string[];
+  onChange?: (values: AdhocFilter[]) => void;
+  value?: AdhocFilter[];
+  datasource?: Datasource;
+  columns?: ColumnMeta[];
+  savedMetrics?: SavedMetric[];
+  selectedMetrics?: string | AdhocMetric | (string | AdhocMetric)[];
+  isLoading?: boolean;
+  canDelete?: (
+    filter: AdhocFilter,
+    allFilters: AdhocFilter[],
+  ) => string | boolean | undefined;
+  theme?: SupersetTheme;
+}
+
+interface FilterOption {
+  column_name?: string;
+  saved_metric_name?: string;
+  label?: string;
+  filterOptionName?: string;
+  [key: string]: unknown;
+}
+
+interface AdhocFilterControlState {
+  values: AdhocFilter[];
+  options: FilterOption[];
+  partitionColumn: string | null;
+}
+
 const { warning } = Modal;
 
 const selectedMetricType = PropTypes.oneOfType([
@@ -78,51 +138,55 @@ const defaultProps = {
   selectedMetrics: [],
 };
 
-function isDictionaryForAdhocFilter(value) {
-  return value && !(value instanceof AdhocFilter) && value.expressionType;
-}
-
-function optionsForSelect(props) {
+function optionsForSelect(props: AdhocFilterControlProps): FilterOption[] {
   const options = [
-    ...props.columns,
+    ...(props.columns || []),
     ...ensureIsArray(props.selectedMetrics).map(
       metric =>
         metric &&
         (typeof metric === 'string'
           ? { saved_metric_name: metric }
-          : new AdhocMetric(metric)),
+          : isDictionaryForAdhocMetric(metric)
+            ? new AdhocMetric(metric)
+            : metric),
     ),
   ].filter(option => option);
 
   return options
-    .reduce((results, option) => {
-      if (option.saved_metric_name) {
+    .reduce<FilterOption[]>((results, option) => {
+      if ((option as FilterOption).saved_metric_name) {
         results.push({
-          ...option,
-          filterOptionName: option.saved_metric_name,
+          ...(option as FilterOption),
+          filterOptionName: (option as FilterOption).saved_metric_name,
         });
-      } else if (option.column_name) {
+      } else if ((option as FilterOption).column_name) {
         results.push({
-          ...option,
-          filterOptionName: `_col_${option.column_name}`,
+          ...(option as FilterOption),
+          filterOptionName: `_col_${(option as FilterOption).column_name}`,
         });
       } else if (option instanceof AdhocMetric) {
         results.push({
           ...option,
           filterOptionName: `_adhocmetric_${option.label}`,
-        });
+        } as FilterOption);
       }
       return results;
     }, [])
-    .sort((a, b) =>
-      (a.saved_metric_name || a.column_name || a.label).localeCompare(
-        b.saved_metric_name || b.column_name || b.label,
+    .sort((a: FilterOption, b: FilterOption) =>
+      (a.saved_metric_name || a.column_name || a.label || '').localeCompare(
+        b.saved_metric_name || b.column_name || b.label || '',
       ),
     );
 }
 
-class AdhocFilterControl extends Component {
-  constructor(props) {
+class AdhocFilterControl extends Component<
+  AdhocFilterControlProps,
+  AdhocFilterControlState
+> {
+  optionRenderer: (option: FilterOption) => JSX.Element;
+  valueRenderer: (adhocFilter: AdhocFilter, index: number) => JSX.Element;
+
+  constructor(props: AdhocFilterControlProps) {
     super(props);
     this.onRemoveFilter = this.onRemoveFilter.bind(this);
     this.onNewFilter = this.onNewFilter.bind(this);
@@ -146,14 +210,14 @@ class AdhocFilterControl extends Component {
         onFilterEdit={this.onFilterEdit}
         options={this.state.options}
         sections={this.props.sections}
-        operators={this.props.operators}
+        operators={this.props.operators as Operators[] | undefined}
         datasource={this.props.datasource}
         onRemoveFilter={e => {
           e.stopPropagation();
           this.onRemoveFilter(index);
         }}
         onMoveLabel={this.moveLabel}
-        onDropLabel={() => this.props.onChange(this.state.values)}
+        onDropLabel={() => this.props.onChange?.(this.state.values)}
         partitionColumn={this.state.partitionColumn}
       />
     );
@@ -206,7 +270,7 @@ class AdhocFilterControl extends Component {
     }
   }
 
-  componentDidUpdate(prevProps) {
+  componentDidUpdate(prevProps: AdhocFilterControlProps): void {
     if (this.props.columns !== prevProps.columns) {
       this.setState({ options: optionsForSelect(this.props) });
     }
@@ -219,17 +283,17 @@ class AdhocFilterControl extends Component {
     }
   }
 
-  removeFilter(index) {
+  removeFilter(index: number): void {
     const valuesCopy = [...this.state.values];
     valuesCopy.splice(index, 1);
     this.setState(prevState => ({
       ...prevState,
       values: valuesCopy,
     }));
-    this.props.onChange(valuesCopy);
+    this.props.onChange?.(valuesCopy);
   }
 
-  onRemoveFilter(index) {
+  onRemoveFilter(index: number): void {
     const { canDelete } = this.props;
     const { values } = this.state;
     const result = canDelete?.(values[index], values);
@@ -240,7 +304,7 @@ class AdhocFilterControl extends Component {
     this.removeFilter(index);
   }
 
-  onNewFilter(newFilter) {
+  onNewFilter(newFilter: FilterOption | AdhocFilter): void {
     const mappedOption = this.mapOption(newFilter);
     if (mappedOption) {
       this.setState(
@@ -249,14 +313,14 @@ class AdhocFilterControl extends Component {
           values: [...prevState.values, mappedOption],
         }),
         () => {
-          this.props.onChange(this.state.values);
+          this.props.onChange?.(this.state.values);
         },
       );
     }
   }
 
-  onFilterEdit(changedFilter) {
-    this.props.onChange(
+  onFilterEdit(changedFilter: AdhocFilter): void {
+    this.props.onChange?.(
       this.state.values.map(value => {
         if (value.filterOptionName === changedFilter.filterOptionName) {
           return changedFilter;
@@ -266,20 +330,21 @@ class AdhocFilterControl extends Component {
     );
   }
 
-  onChange(opts) {
+  onChange(opts: FilterOption[] | null): void {
     const options = (opts || [])
       .map(option => this.mapOption(option))
-      .filter(option => option);
-    this.props.onChange(options);
+      .filter((option): option is AdhocFilter => option !== null);
+    this.props.onChange?.(options);
   }
 
-  getMetricExpression(savedMetricName) {
-    return this.props.savedMetrics.find(
+  getMetricExpression(savedMetricName: string): string {
+    const metric = this.props.savedMetrics?.find(
       savedMetric => savedMetric.metric_name === savedMetricName,
-    ).expression;
+    );
+    return metric?.expression ?? '';
   }
 
-  moveLabel(dragIndex, hoverIndex) {
+  moveLabel(dragIndex: number, hoverIndex: number): void {
     const { values } = this.state;
 
     const newValues = [...values];
@@ -290,7 +355,7 @@ class AdhocFilterControl extends Component {
     this.setState({ values: newValues });
   }
 
-  mapOption(option) {
+  mapOption(option: FilterOption | AdhocFilter): AdhocFilter | null {
     // already a AdhocFilter, skip
     if (option instanceof AdhocFilter) {
       return option;
@@ -331,16 +396,16 @@ class AdhocFilterControl extends Component {
     return null;
   }
 
-  addNewFilterPopoverTrigger(trigger) {
+  addNewFilterPopoverTrigger(trigger: ReactNode): JSX.Element {
     return (
       <AdhocFilterPopoverTrigger
-        operators={this.props.operators}
+        operators={this.props.operators as Operators[] | undefined}
         sections={this.props.sections}
         adhocFilter={new AdhocFilter({})}
-        datasource={this.props.datasource}
+        datasource={(this.props.datasource as Record<string, unknown>) || {}}
         options={this.state.options}
         onFilterEdit={this.onNewFilter}
-        partitionColumn={this.state.partitionColumn}
+        partitionColumn={this.state.partitionColumn ?? undefined}
       >
         {trigger}
       </AdhocFilterPopoverTrigger>
@@ -373,7 +438,10 @@ class AdhocFilterControl extends Component {
   }
 }
 
+// Static properties are defined in the class using static keyword
+// @ts-expect-error - propTypes are defined for runtime validation but TypeScript handles type checking
 AdhocFilterControl.propTypes = propTypes;
+// @ts-expect-error - defaultProps for backward compatibility with PropTypes
 AdhocFilterControl.defaultProps = defaultProps;
 
 export default withTheme(AdhocFilterControl);
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.tsx
similarity index 89%
rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.jsx
rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.tsx
index f7b9e6ddcd..31803e512a 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.jsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/AdhocFilterEditPopover.test.tsx
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type React from 'react';
 import { render, screen, fireEvent } from 'spec/helpers/testing-library';
 import userEvent from '@testing-library/user-event';
 import { AGGREGATES } from 'src/explore/constants';
@@ -39,7 +40,8 @@ const sqlAdhocFilter = new AdhocFilter({
 });
 
 const faultyAdhocFilter = new AdhocFilter({
-  expressionType: null,
+  // Use undefined for faulty expressionType to trigger error state
+  expressionType: undefined,
   subject: null,
   operator: '>',
   comparator: '10',
@@ -69,10 +71,20 @@ const defaultProps = {
   datasource: {},
 };
 
-const renderPopover = (props = {}) =>
-  render(<AdhocFilterEditPopover {...defaultProps} {...props} />, {
-    useRedux: true, // Add Redux provider for context
-  });
+// Cast props to handle AdhocMetric type in options array
+type AdhocFilterEditPopoverComponentProps = React.ComponentProps<
+  typeof AdhocFilterEditPopover
+>;
+const renderPopover = (props: Partial<typeof defaultProps> = {}) =>
+  render(
+    <AdhocFilterEditPopover
+      {...(defaultProps as unknown as AdhocFilterEditPopoverComponentProps)}
+      {...(props as unknown as Partial<AdhocFilterEditPopoverComponentProps>)}
+    />,
+    {
+      useRedux: true, // Add Redux provider for context
+    },
+  );
 
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
 describe('AdhocFilterEditPopover', () => {
@@ -123,7 +135,7 @@ describe('AdhocFilterEditPopover', () => {
     fireEvent.change(sqlInput, { target: { value: 'COUNT(*) > 0' } });
 
     // Wait for validation to complete
-    await screen.findByRole('button', { name: /save/i, disabled: false });
+    await screen.findByRole('button', { name: /save/i });
 
     // Click save button
     const saveButton = screen.getByRole('button', { name: /save/i });
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.tsx
similarity index 74%
rename from superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx
rename to superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.tsx
index 3d5741420a..7bb635594d 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.tsx
@@ -16,8 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { createRef, Component } from 'react';
+import type React from 'react';
+import { createRef, Component, type RefObject } from 'react';
 import PropTypes from 'prop-types';
+import type { SupersetTheme } from '@apache-superset/core/ui';
 import { Button, Icons, Select } from '@superset-ui/core/components';
 import { ErrorBoundary } from 'src/components';
 import { t, SupersetClient } from '@superset-ui/core';
@@ -29,14 +31,54 @@ import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilt
 import AdhocFilterEditPopoverSimpleTabContent from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent';
 import AdhocFilterEditPopoverSqlTabContent from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSqlTabContent';
 import columnType from 'src/explore/components/controls/FilterControl/columnType';
+import type { Dataset } from '@superset-ui/chart-controls';
+import type { ColumnType } from 'src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent';
 import {
   POPOVER_INITIAL_HEIGHT,
   POPOVER_INITIAL_WIDTH,
+  Operators,
 } from 'src/explore/constants';
 import rison from 'rison';
 import { isObject } from 'lodash';
 import { ExpressionTypes } from '../types';
 
+interface LayerOption {
+  id: number | null;
+  value: number;
+  label: string;
+}
+
+interface FilterOption {
+  column_name?: string;
+  saved_metric_name?: string;
+  [key: string]: unknown;
+}
+
+interface AdhocFilterEditPopoverProps {
+  adhocFilter: AdhocFilter;
+  onChange: (filter: AdhocFilter) => void;
+  onClose: () => void;
+  onResize: () => void;
+  options: FilterOption[];
+  datasource?: Record<string, unknown>;
+  partitionColumn?: string;
+  theme?: SupersetTheme;
+  sections?: string[];
+  operators?: string[];
+  requireSave?: boolean;
+}
+
+interface AdhocFilterEditPopoverState {
+  adhocFilter: AdhocFilter;
+  width: number;
+  height: number;
+  activeKey: string;
+  isSimpleTabValid: boolean;
+  selectedLayers: LayerOption[];
+  layerOptions: LayerOption[];
+  hasLayerFilterScopeChanged: boolean;
+}
+
 const propTypes = {
   adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
   onChange: PropTypes.func.isRequired,
@@ -94,8 +136,21 @@ const LayerSelectContainer = styled.div`
   margin-bottom: ${({ theme }) => theme.sizeUnit * 12}px;
 `;
 
-export default class AdhocFilterEditPopover extends Component {
-  constructor(props) {
+export default class AdhocFilterEditPopover extends Component<
+  AdhocFilterEditPopoverProps,
+  AdhocFilterEditPopoverState
+> {
+  popoverContentRef: RefObject<HTMLDivElement>;
+
+  dragStartX = 0;
+
+  dragStartY = 0;
+
+  dragStartWidth = 0;
+
+  dragStartHeight = 0;
+
+  constructor(props: AdhocFilterEditPopoverProps) {
     super(props);
     this.onSave = this.onSave.bind(this);
     this.onDragDown = this.onDragDown.bind(this);
@@ -126,13 +181,15 @@ export default class AdhocFilterEditPopover extends Component {
     document.addEventListener('mouseup', this.onMouseUp);
 
     // Load layer options if deck_slices exist
-    if (
-      this.props.adhocFilter?.deck_slices &&
-      this.props.adhocFilter.deck_slices.length > 0
-    ) {
+    const deckSlices = this.props.adhocFilter?.deck_slices as
+      | number[]
+      | undefined;
+    if (deckSlices && deckSlices.length > 0) {
       this.loadLayerOptions(0, 100).then(result => {
         this.setState({ layerOptions: result.data });
-        const layerFilterScope = this.props.adhocFilter?.layerFilterScope;
+        const layerFilterScope = this.props.adhocFilter?.layerFilterScope as
+          | number[]
+          | undefined;
         if (layerFilterScope) {
           const selectedLayers = layerFilterScope.map(item => {
             const layerOption = result.data.find(
@@ -140,7 +197,9 @@ export default class AdhocFilterEditPopover extends Component {
             );
             return layerOption;
           });
-          this.setState({ selectedLayers });
+          this.setState({
+            selectedLayers: selectedLayers.filter(Boolean) as LayerOption[],
+          });
         }
       });
     }
@@ -151,18 +210,19 @@ export default class AdhocFilterEditPopover extends Component {
     document.removeEventListener('mousemove', this.onMouseMove);
   }
 
-  onAdhocFilterChange(adhocFilter) {
+  onAdhocFilterChange(adhocFilter: AdhocFilter): void {
     this.setState({ adhocFilter });
   }
 
-  setSimpleTabIsValid(isValid) {
+  setSimpleTabIsValid(isValid: boolean): void {
     this.setState({ isSimpleTabValid: isValid });
   }
 
   onSave() {
-    const hasDeckSlices =
-      this.state.adhocFilter.deck_slices &&
-      this.state.adhocFilter.deck_slices.length > 0;
+    const deckSlices = this.state.adhocFilter.deck_slices as
+      | number[]
+      | undefined;
+    const hasDeckSlices = deckSlices && deckSlices.length > 0;
 
     if (!hasDeckSlices) {
       this.props.onChange(this.state.adhocFilter);
@@ -176,16 +236,15 @@ export default class AdhocFilterEditPopover extends Component {
       }
       return item;
     });
-    const correctedAdhocFilter = {
-      ...this.state.adhocFilter,
+    const correctedAdhocFilter = this.state.adhocFilter.duplicateWith({
       layerFilterScope: selectedLayers,
-    };
+    });
     this.setState({ hasLayerFilterScopeChanged: false });
     this.props.onChange(correctedAdhocFilter);
     this.props.onClose();
   }
 
-  onDragDown(e) {
+  onDragDown(e: React.MouseEvent): void {
     this.dragStartX = e.clientX;
     this.dragStartY = e.clientY;
     this.dragStartWidth = this.state.width;
@@ -193,7 +252,7 @@ export default class AdhocFilterEditPopover extends Component {
     document.addEventListener('mousemove', this.onMouseMove);
   }
 
-  onMouseMove(e) {
+  onMouseMove(e: MouseEvent): void {
     this.props.onResize();
     this.setState({
       width: Math.max(
@@ -211,17 +270,17 @@ export default class AdhocFilterEditPopover extends Component {
     document.removeEventListener('mousemove', this.onMouseMove);
   }
 
-  onTabChange(activeKey) {
+  onTabChange(activeKey: string) {
     this.setState({
       activeKey,
     });
   }
 
-  adjustHeight(heightDifference) {
+  adjustHeight(heightDifference: number) {
     this.setState(state => ({ height: state.height + heightDifference }));
   }
 
-  loadLayerOptions(page, pageSize) {
+  loadLayerOptions(page: number, pageSize: number) {
     const query = rison.encode({
       columns: ['id', 'slice_name', 'viz_type'],
       filters: [{ col: 'viz_type', opr: 'sw', value: 'deck' }],
@@ -247,7 +306,8 @@ export default class AdhocFilterEditPopover extends Component {
         };
       }
 
-      const deckSlices = this.props.adhocFilter?.deck_slices || [];
+      const deckSlices = (this.props.adhocFilter?.deck_slices ||
+        []) as number[];
 
       const list = [
         {
@@ -256,7 +316,7 @@ export default class AdhocFilterEditPopover extends Component {
           label: 'All',
         },
         ...response.json.result
-          .map(item => {
+          .map((item: { id: number; slice_name: string }) => {
             const sliceIndex = deckSlices.indexOf(item.id);
             return {
               id: item.id,
@@ -265,8 +325,18 @@ export default class AdhocFilterEditPopover extends Component {
               sliceIndex,
             };
           })
-          .filter(item => item.sliceIndex !== -1)
-          .map(({ sliceIndex, ...item }) => item),
+          .filter((item: { sliceIndex: number }) => item.sliceIndex !== -1)
+          .map(
+            ({
+              sliceIndex,
+              ...item
+            }: {
+              sliceIndex: number;
+              id: number;
+              value: number;
+              label: string;
+            }) => item,
+          ),
       ];
 
       return {
@@ -276,24 +346,29 @@ export default class AdhocFilterEditPopover extends Component {
     });
   }
 
-  onLayerChange(selectedValue) {
-    let updatedSelectedLayers = selectedValue;
+  onLayerChange(selectedValue: LayerOption[] | number[] | null) {
+    let updatedSelectedLayers: LayerOption[] =
+      (selectedValue as LayerOption[]) || [];
 
     if (!selectedValue || selectedValue.length === 0) {
       updatedSelectedLayers = [{ id: null, value: -1, label: 'All' }];
     } else if (
       selectedValue.length > 1 &&
-      selectedValue.some(item => item.value === -1 || item === -1)
+      selectedValue.some(
+        (item: LayerOption | number) =>
+          (typeof item === 'object' && item.value === -1) || item === -1,
+      )
     ) {
+      const lastItem = selectedValue[selectedValue.length - 1];
       if (
-        selectedValue[selectedValue.length - 1].value === -1 ||
-        selectedValue[selectedValue.length - 1] === -1
+        (typeof lastItem === 'object' && lastItem.value === -1) ||
+        lastItem === -1
       ) {
         updatedSelectedLayers = [{ id: null, value: -1, label: 'All' }];
       } else {
-        updatedSelectedLayers = selectedValue
-          .filter(item => item.value !== -1)
-          .filter(item => item !== -1);
+        updatedSelectedLayers = (selectedValue as LayerOption[]).filter(
+          (item: LayerOption) => item.value !== -1,
+        );
       }
     }
 
@@ -324,8 +399,8 @@ export default class AdhocFilterEditPopover extends Component {
       !adhocFilter.equals(propsAdhocFilter) ||
       hasLayerFilterScopeChanged;
 
-    const hasDeckSlices =
-      adhocFilter.deck_slices && adhocFilter.deck_slices.length > 0;
+    const renderDeckSlices = adhocFilter.deck_slices as number[] | undefined;
+    const hasDeckSlices = renderDeckSlices && renderDeckSlices.length > 0;
 
     return (
       <FilterPopoverContentContainer
@@ -349,11 +424,11 @@ export default class AdhocFilterEditPopover extends Component {
               children: (
                 <ErrorBoundary>
                   <AdhocFilterEditPopoverSimpleTabContent
-                    operators={operators}
+                    operators={operators as Operators[] | undefined}
                     adhocFilter={this.state.adhocFilter}
                     onChange={this.onAdhocFilterChange}
-                    options={options}
-                    datasource={datasource}
+                    options={options as ColumnType[]}
+                    datasource={datasource as unknown as Dataset}
                     onHeightChange={this.adjustHeight}
                     partitionColumn={partitionColumn}
                     popoverRef={this.popoverContentRef.current}
@@ -372,7 +447,6 @@ export default class AdhocFilterEditPopover extends Component {
                     onChange={this.onAdhocFilterChange}
                     options={this.props.options}
                     height={this.state.height}
-                    activeKey={this.state.activeKey}
                     datasource={datasource}
                   />
                 </ErrorBoundary>
@@ -384,7 +458,9 @@ export default class AdhocFilterEditPopover extends Component {
           <LayerSelectContainer>
             <Select
               options={this.state.layerOptions}
-              onChange={this.onLayerChange}
+              onChange={
+                this.onLayerChange as unknown as (value: unknown) => void
+              }
               value={selectedLayers}
               mode="multiple"
             />
@@ -427,4 +503,5 @@ export default class AdhocFilterEditPopover extends Component {
   }
 }
 
+// @ts-expect-error - propTypes are defined for runtime validation but TypeScript handles type checking
 AdhocFilterEditPopover.propTypes = propTypes;
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
index 2848d88573..d15aef3750 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/AdhocFilterEditPopoverSimpleTabContent.test.tsx
@@ -41,6 +41,7 @@ import fetchMock from 'fetch-mock';
 import { TestDataset, Dataset } from '@superset-ui/chart-controls';
 import AdhocFilterEditPopoverSimpleTabContent, {
   useSimpleTabFilterProps,
+  Props,
 } from '.';
 import { Clauses, ExpressionTypes } from '../types';
 
@@ -56,10 +57,10 @@ const simpleAdhocFilter = new AdhocFilter({
 const advancedTypeTestAdhocFilterTest = new AdhocFilter({
   expressionType: ExpressionTypes.Simple,
   subject: 'advancedDataType',
-  operatorId: null,
-  operator: null,
-  comparator: null,
-  clause: null,
+  operatorId: undefined,
+  operator: undefined,
+  comparator: undefined,
+  clause: undefined,
 });
 
 const simpleMultiAdhocFilter = new AdhocFilter({
@@ -93,7 +94,7 @@ const options = [
   sumValueAdhocMetric,
 ];
 
-const getAdvancedDataTypeTestProps = (overrides?: Record<string, any>) => {
+const getAdvancedDataTypeTestProps = (overrides?: Record<string, unknown>) => {
   const onChange = sinon.spy();
   const validHandler = sinon.spy();
   const props = {
@@ -113,7 +114,7 @@ const getAdvancedDataTypeTestProps = (overrides?: Record<string, any>) => {
   return props;
 };
 
-function setup(overrides?: Record<string, any>) {
+function setup(overrides?: Record<string, unknown>) {
   const onChange = sinon.spy();
   const validHandler = sinon.spy();
   const spy = jest.spyOn(redux, 'useSelector');
@@ -132,7 +133,9 @@ function setup(overrides?: Record<string, any>) {
     ...overrides,
     validHandler,
   };
-  render(<AdhocFilterEditPopoverSimpleTabContent {...props} />);
+  render(
+    <AdhocFilterEditPopoverSimpleTabContent {...(props as unknown as Props)} />,
+  );
   return props;
 }
 
@@ -193,10 +196,10 @@ test('shows boolean only operators when subject is boolean', () => {
     adhocFilter: new AdhocFilter({
       expressionType: ExpressionTypes.Simple,
       subject: 'value',
-      operatorId: null,
-      operator: null,
-      comparator: null,
-      clause: null,
+      operatorId: undefined,
+      operator: undefined,
+      comparator: undefined,
+      clause: undefined,
     }),
     datasource: {
       columns: [
@@ -208,7 +211,9 @@ test('shows boolean only operators when subject is boolean', () => {
       ],
     },
   });
-  const { isOperatorRelevant } = useSimpleTabFilterProps(props);
+  const { isOperatorRelevant } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   [
     Operators.IsTrue,
     Operators.IsFalse,
@@ -222,10 +227,10 @@ test('shows boolean only operators when subject is number', () => {
     adhocFilter: new AdhocFilter({
       expressionType: ExpressionTypes.Simple,
       subject: 'value',
-      operatorId: null,
-      operator: null,
-      comparator: null,
-      clause: null,
+      operatorId: undefined,
+      operator: undefined,
+      comparator: undefined,
+      clause: undefined,
     }),
     datasource: {
       columns: [
@@ -237,7 +242,9 @@ test('shows boolean only operators when subject is number', () => {
       ],
     },
   });
-  const { isOperatorRelevant } = useSimpleTabFilterProps(props);
+  const { isOperatorRelevant } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   [
     Operators.IsTrue,
     Operators.IsFalse,
@@ -248,7 +255,9 @@ test('shows boolean only operators when subject is number', () => {
 
 test('will convert from individual comparator to array if the operator changes to multi', () => {
   const props = setup();
-  const { onOperatorChange } = useSimpleTabFilterProps(props);
+  const { onOperatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   onOperatorChange(Operators.In);
   expect(props.onChange.calledOnce).toBe(true);
   expect(props.onChange.lastCall.args[0].comparator).toEqual(['10']);
@@ -259,7 +268,9 @@ test('will convert from array to individual comparators if the operator changes
   const props = setup({
     adhocFilter: simpleMultiAdhocFilter,
   });
-  const { onOperatorChange } = useSimpleTabFilterProps(props);
+  const { onOperatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   onOperatorChange(Operators.LessThan);
   expect(props.onChange.calledOnce).toBe(true);
   expect(props.onChange.lastCall.args[0]).toEqual(
@@ -273,7 +284,9 @@ test('will convert from array to individual comparators if the operator changes
 
 test('passes the new adhocFilter to onChange after onComparatorChange', () => {
   const props = setup();
-  const { onComparatorChange } = useSimpleTabFilterProps(props);
+  const { onComparatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   onComparatorChange('20');
   expect(props.onChange.calledOnce).toBe(true);
   expect(props.onChange.lastCall.args[0]).toEqual(
@@ -283,7 +296,9 @@ test('passes the new adhocFilter to onChange after onComparatorChange', () => {
 
 test('will filter operators for table datasources', () => {
   const props = setup({ datasource: { type: 'table' as const } });
-  const { isOperatorRelevant } = useSimpleTabFilterProps(props);
+  const { isOperatorRelevant } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   expect(isOperatorRelevant(Operators.Like, 'value')).toBe(true);
 });
 
@@ -297,7 +312,9 @@ test('will show LATEST PARTITION operator', () => {
     adhocFilter: simpleCustomFilter,
     partitionColumn: 'ds',
   });
-  const { isOperatorRelevant } = useSimpleTabFilterProps(props);
+  const { isOperatorRelevant } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   expect(isOperatorRelevant(Operators.LatestPartition, 'ds')).toBe(true);
   expect(isOperatorRelevant(Operators.LatestPartition, 'value')).toBe(false);
 });
@@ -316,7 +333,9 @@ test('will generate custom sqlExpression for LATEST PARTITION operator', () => {
     adhocFilter: testAdhocFilter,
     partitionColumn: 'ds',
   });
-  const { onOperatorChange } = useSimpleTabFilterProps(props);
+  const { onOperatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   onOperatorChange(Operators.LatestPartition);
   expect(props.onChange.calledOnce).toBe(true);
   expect(props.onChange.lastCall.args[0]).toEqual(
@@ -342,7 +361,9 @@ test('will not display boolean operators when column type is string', () => {
     },
     adhocFilter: simpleAdhocFilter,
   });
-  const { isOperatorRelevant } = useSimpleTabFilterProps(props);
+  const { isOperatorRelevant } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   const booleanOnlyOperators = [Operators.IsTrue, Operators.IsFalse];
   booleanOnlyOperators.forEach(operator => {
     expect(isOperatorRelevant(operator, 'value')).toBe(false);
@@ -364,7 +385,9 @@ test('will display boolean operators when column is an expression', () => {
     },
     adhocFilter: simpleAdhocFilter,
   });
-  const { isOperatorRelevant } = useSimpleTabFilterProps(props);
+  const { isOperatorRelevant } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   const booleanOnlyOperators = [Operators.IsTrue, Operators.IsFalse];
   booleanOnlyOperators.forEach(operator => {
     expect(isOperatorRelevant(operator, 'value')).toBe(true);
@@ -373,7 +396,9 @@ test('will display boolean operators when column is an expression', () => {
 
 test('sets comparator to undefined when operator is IS_TRUE', () => {
   const props = setup();
-  const { onOperatorChange } = useSimpleTabFilterProps(props);
+  const { onOperatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   onOperatorChange(Operators.IsTrue);
   expect(props.onChange.calledOnce).toBe(true);
   expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsTrue);
@@ -383,7 +408,9 @@ test('sets comparator to undefined when operator is IS_TRUE', () => {
 
 test('sets comparator to undefined when operator is IS_FALSE', () => {
   const props = setup();
-  const { onOperatorChange } = useSimpleTabFilterProps(props);
+  const { onOperatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   onOperatorChange(Operators.IsFalse);
   expect(props.onChange.calledOnce).toBe(true);
   expect(props.onChange.lastCall.args[0].operatorId).toBe(Operators.IsFalse);
@@ -393,7 +420,9 @@ test('sets comparator to undefined when operator is IS_FALSE', () => {
 
 test('sets comparator to undefined when operator is IS_NULL or IS_NOT_NULL', () => {
   const props = setup();
-  const { onOperatorChange } = useSimpleTabFilterProps(props);
+  const { onOperatorChange } = useSimpleTabFilterProps(
+    props as unknown as Props,
+  );
   [Operators.IsNull, Operators.IsNotNull].forEach(op => {
     onOperatorChange(op);
     expect(props.onChange.called).toBe(true);
@@ -409,9 +438,14 @@ test('should not call API when column has no advanced data type', async () => {
   const props = getAdvancedDataTypeTestProps();
 
   await act(async () => {
-    render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
-      store,
-    });
+    render(
+      <AdhocFilterEditPopoverSimpleTabContent
+        {...(props as unknown as Props)}
+      />,
+      {
+        store,
+      },
+    );
   });
 
   const filterValueField = screen.getByPlaceholderText(
@@ -443,9 +477,14 @@ test('should call API when column has advanced data type', async () => {
   });
 
   await act(async () => {
-    render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
-      store,
-    });
+    render(
+      <AdhocFilterEditPopoverSimpleTabContent
+        {...(props as unknown as Props)}
+      />,
+      {
+        store,
+      },
+    );
   });
 
   const filterValueField = screen.getByPlaceholderText(
@@ -478,9 +517,14 @@ test('save button should be disabled if error message from API is returned', asy
   });
 
   await act(async () => {
-    render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
-      store,
-    });
+    render(
+      <AdhocFilterEditPopoverSimpleTabContent
+        {...(props as unknown as Props)}
+      />,
+      {
+        store,
+      },
+    );
   });
 
   const filterValueField = screen.getByPlaceholderText(
@@ -515,9 +559,14 @@ test('advanced data type operator list should update after API response', async
   });
 
   await act(async () => {
-    render(<AdhocFilterEditPopoverSimpleTabContent {...props} />, {
-      store,
-    });
+    render(
+      <AdhocFilterEditPopoverSimpleTabContent
+        {...(props as unknown as Props)}
+      />,
+      {
+        store,
+      },
+    );
   });
 
   const filterValueField = screen.getByPlaceholderText(
@@ -581,7 +630,9 @@ test('dropdown should remain open when clicked after filter is configured', asyn
     validHandler,
   };
 
-  render(<AdhocFilterEditPopoverSimpleTabContent {...props} />);
+  render(
+    <AdhocFilterEditPopoverSimpleTabContent {...(props as unknown as Props)} />,
+  );
 
   const operatorDropdown = screen.getByRole('combobox', {
     name: 'Select operator',
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/index.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/index.tsx
index bd8c7cb30d..12d8b109d5 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/index.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopoverSimpleTabContent/index.tsx
@@ -18,7 +18,13 @@
  */
 import { FC, ChangeEvent, useEffect, useState, useRef } from 'react';
 
-import { Input, InputRef, Select, Tooltip } from '@superset-ui/core/components';
+import {
+  Input,
+  InputRef,
+  Select,
+  Tooltip,
+  type SelectValue,
+} from '@superset-ui/core/components';
 import {
   isFeatureEnabled,
   FeatureFlag,
@@ -87,9 +93,11 @@ export interface Props {
   onChange: (filter: AdhocFilter) => void;
   options: ColumnType[];
   datasource: Dataset;
-  partitionColumn: string;
+  partitionColumn?: string;
   operators?: Operators[];
   validHandler: (isValid: boolean) => void;
+  onHeightChange?: (heightDifference: number) => void;
+  popoverRef?: HTMLDivElement | null;
 }
 
 export interface AdvancedDataTypesState {
@@ -151,7 +159,9 @@ export const useSimpleTabFilterProps = (props: Props) => {
     }
     let { operator, operatorId, comparator } = props.adhocFilter;
     operator =
-      operator && operatorId && isOperatorRelevant(operatorId, subject)
+      operator &&
+      operatorId &&
+      isOperatorRelevant(operatorId as Operators, subject)
         ? OPERATOR_ENUM_TO_OPERATOR_TYPE[
             operatorId as keyof typeof OPERATOR_ENUM_TO_OPERATOR_TYPE
           ].operation
@@ -290,7 +300,17 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
   };
 
   const renderSubjectOptionLabel = (option: ColumnType) => (
-    <FilterDefinitionOption option={option} />
+    <FilterDefinitionOption
+      option={
+        option as unknown as {
+          column_name?: string;
+          saved_metric_name?: string;
+          label?: string;
+          type?: string;
+          [key: string]: unknown;
+        }
+      }
+    />
   );
 
   const getOptionsRemaining = () => {
@@ -314,9 +334,16 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
   let columns = props.options;
   const { subject, operator, operatorId } = props.adhocFilter;
 
+  const subjectValue =
+    typeof subject === 'string'
+      ? subject
+      : subject && 'column_name' in subject
+        ? subject.column_name
+        : undefined;
+
   const subjectSelectProps = {
     ariaLabel: t('Select subject'),
-    value: subject ?? undefined,
+    value: subjectValue,
     onChange: handleSubjectChange,
     notFoundContent: t(
       'No such column found. To filter on a metric, try the Custom SQL tab.',
@@ -333,11 +360,12 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
     option => 'column_name' in option && option.column_name,
   );
 
+  const subjectString = typeof subject === 'string' ? subject : '';
   const operatorSelectProps = {
     placeholder: t(
       '%s operator(s)',
       (props.operators ?? OPERATORS_OPTIONS).filter(op =>
-        isOperatorRelevantWrapper(op, subject),
+        isOperatorRelevantWrapper(op, subjectString),
       ).length,
     ),
     value: operatorId,
@@ -353,25 +381,35 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
     allowClear: true,
     allowNewOptions: true,
     ariaLabel: t('Comparator option'),
-    mode: MULTI_OPERATORS.has(operatorId)
-      ? ('multiple' as const)
-      : ('single' as const),
+    mode:
+      operatorId && MULTI_OPERATORS.has(operatorId as Operators)
+        ? ('multiple' as const)
+        : ('single' as const),
     loading: loadingComparatorSuggestions,
-    value: comparator,
+    value: comparator as SelectValue,
     onChange: onComparatorChange,
     notFoundContent: t('Type a value here'),
-    disabled: DISABLE_INPUT_OPERATORS.includes(operatorId),
+    disabled:
+      operatorId !== undefined &&
+      DISABLE_INPUT_OPERATORS.includes(operatorId as Operators),
     placeholder: createSuggestionsPlaceholder(),
   };
 
-  const labelText =
-    comparator && comparator.length > 0 && createSuggestionsPlaceholder();
+  const comparatorHasValue =
+    comparator &&
+    (Array.isArray(comparator)
+      ? comparator.length > 0
+      : String(comparator).length > 0);
+  const labelText = comparatorHasValue ? createSuggestionsPlaceholder() : '';
 
   const datePicker = useDatePickerInAdhocFilter({
-    columnName: props.adhocFilter.subject,
+    columnName:
+      typeof props.adhocFilter.subject === 'string'
+        ? props.adhocFilter.subject
+        : undefined,
     timeRange:
       props.adhocFilter.operator === Operators.TemporalRange
-        ? props.adhocFilter.comparator
+        ? (props.adhocFilter.comparator as string | undefined)
         : undefined,
     datasource: props.datasource,
     onChange: onDatePickerChange,
@@ -441,8 +479,14 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
 
   useEffect(() => {
     if (isFeatureEnabled(FeatureFlag.EnableAdvancedDataTypes)) {
+      const comparatorValue =
+        comparator === undefined
+          ? ''
+          : typeof comparator === 'string'
+            ? comparator
+            : String(comparator);
       fetchAdvancedDataTypeValueCallback(
-        comparator === undefined ? '' : comparator,
+        comparatorValue,
         advancedDataTypesState,
         subjectAdvancedDataType,
       );
@@ -501,7 +545,7 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
     <>
       <Select
         options={(props.operators ?? OPERATORS_OPTIONS)
-          .filter(op => isOperatorRelevantWrapper(op, subject))
+          .filter(op => isOperatorRelevantWrapper(op, subjectString))
           .map((option, index) => ({
             value: option,
             label: OPERATOR_ENUM_TO_OPERATOR_TYPE[option].display,
@@ -510,7 +554,8 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
           }))}
         {...operatorSelectProps}
       />
-      {MULTI_OPERATORS.has(operatorId) || suggestions.length > 0 ? (
+      {(operatorId && MULTI_OPERATORS.has(operatorId as Operators)) ||
+      suggestions.length > 0 ? (
         <Tooltip
           title={
             advancedDataTypesState.errorMessage ||
@@ -543,9 +588,12 @@ const AdhocFilterEditPopoverSimpleTabContent: FC<Props> = props => {
             name="filter-value"
             ref={comparatorInputRef}
             onChange={onInputComparatorChange}
-            value={comparator}
+            value={typeof comparator === 'string' ? comparator : undefined}
             placeholder={t('Filter value (case sensitive)')}
-            disabled={DISABLE_INPUT_OPERATORS.includes(operatorId)}
+            disabled={
+              operatorId !== undefined &&
+              DISABLE_INPUT_OPERATORS.includes(operatorId as Operators)
+            }
           />
         </Tooltip>
       )}
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterOption/index.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterOption/index.tsx
index 5852baf776..ca406143ec 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterOption/index.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterOption/index.tsx
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type React from 'react';
 import { OptionControlLabel } from 'src/explore/components/controls/OptionControls';
 import { DndItemType } from 'src/explore/components/DndItemType';
 import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
@@ -26,14 +27,14 @@ import { useGetTimeRangeLabel } from '../utils';
 
 export interface AdhocFilterOptionProps {
   adhocFilter: AdhocFilter;
-  onFilterEdit: () => void;
-  onRemoveFilter: () => void;
+  onFilterEdit: (editedFilter: AdhocFilter) => void;
+  onRemoveFilter: (e: React.MouseEvent) => void;
   options: OptionSortType[];
-  sections: string[];
-  operators: Operators[];
-  datasource: Record<string, any>;
-  partitionColumn: string;
-  onMoveLabel: () => void;
+  sections?: string[];
+  operators?: Operators[];
+  datasource?: Record<string, unknown>;
+  partitionColumn?: string | null;
+  onMoveLabel: (dragIndex: number, hoverIndex: number) => void;
   onDropLabel: () => void;
   index: number;
 }
@@ -59,14 +60,18 @@ export default function AdhocFilterOption({
       operators={operators}
       adhocFilter={adhocFilter}
       options={options}
-      datasource={datasource}
+      datasource={(datasource as Record<string, unknown>) || {}}
       onFilterEdit={onFilterEdit}
-      partitionColumn={partitionColumn}
+      partitionColumn={partitionColumn ?? undefined}
     >
       <OptionControlLabel
         label={actualTimeRange ?? adhocFilter.getDefaultLabel()}
         tooltipTitle={title ?? adhocFilter.getTooltipTitle()}
-        onRemove={onRemoveFilter}
+        onRemove={() =>
+          onRemoveFilter({
+            stopPropagation: () => {},
+          } as React.MouseEvent<Element, MouseEvent>)
+        }
         onMoveLabel={onMoveLabel}
         onDropLabel={onDropLabel}
         index={index}
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger/index.tsx b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger/index.tsx
index 2a2670ee8c..115fb9449a 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger/index.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger/index.tsx
@@ -93,7 +93,7 @@ class AdhocFilterPopoverTrigger extends PureComponent<
           datasource={this.props.datasource}
           partitionColumn={this.props.partitionColumn}
           onResize={this.onPopoverResize}
-          onClose={closePopover}
+          onClose={closePopover ?? (() => {})}
           sections={this.props.sections}
           operators={this.props.operators}
           onChange={this.props.onFilterEdit}
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/utils/useDatePickerInAdhocFilter.tsx b/superset-frontend/src/explore/components/controls/FilterControl/utils/useDatePickerInAdhocFilter.tsx
index af148bb92d..8d2f895fb5 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/utils/useDatePickerInAdhocFilter.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/utils/useDatePickerInAdhocFilter.tsx
@@ -24,7 +24,7 @@ import DateFilterControl from 'src/explore/components/controls/DateFilterControl
 import ControlHeader from 'src/explore/components/ControlHeader';
 
 interface DatePickerInFilterProps {
-  columnName: string;
+  columnName?: string;
   timeRange?: string;
   datasource: Dataset;
   onChange: (columnName: string, timeRange: string) => void;
@@ -36,7 +36,7 @@ export const useDatePickerInAdhocFilter = ({
   datasource,
   onChange,
 }: DatePickerInFilterProps): ReactElement | undefined => {
-  const onTimeRangeChange = (val: string) => onChange(columnName, val);
+  const onTimeRangeChange = (val: string) => onChange(columnName ?? '', val);
 
   const extensionsRegistry = getExtensionsRegistry();
 
@@ -45,7 +45,7 @@ export const useDatePickerInAdhocFilter = ({
   );
   const DateFilterComponent = DateFilterControlExtension ?? DateFilterControl;
 
-  return isTemporalColumn(columnName, datasource) ? (
+  return columnName && isTemporalColumn(columnName, datasource) ? (
     <>
       <ControlHeader label={t('Time Range')} />
       <DateFilterComponent
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx b/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx
index d68239c709..7fa0c8cdef 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx
+++ b/superset-frontend/src/explore/components/controls/FilterControl/utils/useGetTimeRangeLabel.tsx
@@ -53,21 +53,22 @@ export const useGetTimeRangeLabel = (adhocFilter: AdhocFilter): Results => {
       adhocFilter.comparator !== NO_TIME_RANGE &&
       actualTimeRange.title !== adhocFilter.comparator
     ) {
-      fetchTimeRange(adhocFilter.comparator, adhocFilter.subject).then(
-        ({ value, error }) => {
-          if (error) {
-            setActualTimeRange({
-              actualTimeRange: `${adhocFilter.subject} (${adhocFilter.comparator})`,
-              title: error,
-            });
-          } else {
-            setActualTimeRange({
-              actualTimeRange: value ?? '',
-              title: adhocFilter.comparator,
-            });
-          }
-        },
-      );
+      fetchTimeRange(
+        adhocFilter.comparator as string,
+        adhocFilter.subject as string,
+      ).then(({ value, error }) => {
+        if (error) {
+          setActualTimeRange({
+            actualTimeRange: `${adhocFilter.subject} (${adhocFilter.comparator})`,
+            title: error,
+          });
+        } else {
+          setActualTimeRange({
+            actualTimeRange: value ?? '',
+            title: adhocFilter.comparator as string | undefined,
+          });
+        }
+      });
     }
   }, [adhocFilter]);
 
diff --git a/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx b/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx
similarity index 69%
rename from superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx
rename to superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx
index aa405a0ab5..fb8a1ab945 100644
--- a/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx
@@ -28,7 +28,42 @@ import PopoverSection from '@superset-ui/core/components/PopoverSection';
 const controlTypes = {
   fixed: 'fix',
   metric: 'metric',
-};
+} as const;
+
+interface ControlValue {
+  type?: 'fix' | 'metric';
+  value?:
+    | string
+    | number
+    | { label?: string; expressionType?: string; sqlExpression?: string };
+}
+
+interface MetricValue {
+  label?: string;
+  expressionType?: string;
+  sqlExpression?: string;
+  [key: string]: unknown;
+}
+
+interface DatasourceType {
+  columns?: { column_name: string }[];
+  metrics?: { metric_name: string; expression: string }[];
+  [key: string]: unknown;
+}
+
+interface FixedOrMetricControlProps {
+  onChange?: (value: ControlValue) => void;
+  value?: ControlValue;
+  isFloat?: boolean;
+  datasource: DatasourceType;
+  default?: ControlValue;
+}
+
+interface FixedOrMetricControlState {
+  type: 'fix' | 'metric';
+  fixedValue: string | number;
+  metricValue: MetricValue | null;
+}
 
 const propTypes = {
   onChange: PropTypes.func,
@@ -46,50 +81,60 @@ const defaultProps = {
   default: { type: controlTypes.fixed, value: 5 },
 };
 
-export default class FixedOrMetricControl extends Component {
-  constructor(props) {
+export default class FixedOrMetricControl extends Component<
+  FixedOrMetricControlProps,
+  FixedOrMetricControlState
+> {
+  constructor(props: FixedOrMetricControlProps) {
     super(props);
     this.onChange = this.onChange.bind(this);
     this.setType = this.setType.bind(this);
     this.setFixedValue = this.setFixedValue.bind(this);
     this.setMetric = this.setMetric.bind(this);
-    const type =
-      (props.value ? props.value.type : props.default.type) ||
-      controlTypes.fixed;
-    const value =
-      (props.value ? props.value.value : props.default.value) || '100';
+    const type = (props.value?.type ??
+      props.default?.type ??
+      controlTypes.fixed) as 'fix' | 'metric';
+    const rawValue = props.value?.value ?? props.default?.value ?? '100';
+    const fixedValue =
+      type === controlTypes.fixed && typeof rawValue !== 'object'
+        ? rawValue
+        : '';
+    const metricValue =
+      type === controlTypes.metric && typeof rawValue === 'object'
+        ? (rawValue as MetricValue)
+        : null;
     this.state = {
       type,
-      fixedValue: type === controlTypes.fixed ? value : '',
-      metricValue: type === controlTypes.metric ? value : null,
+      fixedValue,
+      metricValue,
     };
   }
 
-  onChange() {
-    this.props.onChange({
+  onChange(): void {
+    this.props.onChange?.({
       type: this.state.type,
       value:
         this.state.type === controlTypes.fixed
           ? this.state.fixedValue
-          : this.state.metricValue,
+          : (this.state.metricValue ?? undefined),
     });
   }
 
-  setType(type) {
+  setType(type: 'fix' | 'metric'): void {
     this.setState({ type }, this.onChange);
   }
 
-  setFixedValue(fixedValue) {
+  setFixedValue(fixedValue: string | number): void {
     this.setState({ fixedValue }, this.onChange);
   }
 
-  setMetric(metricValue) {
+  setMetric(metricValue: MetricValue | null): void {
     this.setState({ metricValue }, this.onChange);
   }
 
   render() {
-    const value = this.props.value || this.props.default;
-    const type = value.type || controlTypes.fixed;
+    const value = this.props.value ?? this.props.default;
+    const type = value?.type ?? controlTypes.fixed;
     const columns = this.props.datasource
       ? this.props.datasource.columns
       : null;
@@ -136,6 +181,7 @@ export default class FixedOrMetricControl extends Component {
                       onChange={this.setFixedValue}
                       onFocus={() => {
                         this.setType(controlTypes.fixed);
+                        return {};
                       }}
                       value={this.state.fixedValue}
                     />
@@ -149,8 +195,8 @@ export default class FixedOrMetricControl extends Component {
                   >
                     <MetricsControl
                       name="metric"
-                      columns={columns}
-                      savedMetrics={metrics}
+                      columns={columns ?? undefined}
+                      savedMetrics={metrics ?? undefined}
                       multi={false}
                       onFocus={() => {
                         this.setType(controlTypes.metric);
@@ -170,5 +216,7 @@ export default class FixedOrMetricControl extends Component {
   }
 }
 
+// @ts-expect-error - propTypes are defined for runtime validation but TypeScript handles type checking
 FixedOrMetricControl.propTypes = propTypes;
+// @ts-expect-error - defaultProps for backward compatibility with PropTypes
 FixedOrMetricControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.js b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.ts
similarity index 98%
rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.js
rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.ts
index 89ba8eacbe..50c819248f 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.js
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.ts
@@ -203,14 +203,14 @@ describe('AdhocMetric', () => {
       aggregate: AGGREGATES.SUM,
     });
     expect(adhocMetric2.aggregate).toBe(AGGREGATES.SUM);
-    expect(adhocMetric2.column.column_name).toBe('my_column');
+    expect(adhocMetric2.column?.column_name).toBe('my_column');
 
     const adhocMetric3 = adhocMetric.duplicateWith({
       expressionType: EXPRESSION_TYPES.SIMPLE,
       column: valueColumn,
     });
     expect(adhocMetric3.aggregate).toBe(AGGREGATES.AVG);
-    expect(adhocMetric3.column.column_name).toBe('value');
+    expect(adhocMetric3.column?.column_name).toBe('value');
   });
 
   test('should transform count_distinct SQL and do not change label if does not set metric label', () => {
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts
similarity index 62%
rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js
rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts
index 20c14793f7..7bd04bd0f1 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts
@@ -16,6 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { AdhocMetric as CoreAdhocMetric } from '@superset-ui/core';
 import {
   sqlaAutoGeneratedMetricRegex,
   AGGREGATES,
@@ -26,7 +27,33 @@ export const EXPRESSION_TYPES = {
   SQL: 'SQL',
 };
 
-function inferSqlExpressionColumn(adhocMetric) {
+interface ColumnType {
+  column_name: string;
+  verbose_name?: string;
+  // Allow additional properties from ColumnMeta and other column types
+  [key: string]: unknown;
+}
+
+interface AdhocMetricInput {
+  expressionType?: string;
+  column?: ColumnType | null;
+  aggregate?: string | null;
+  sqlExpression?: string | null;
+  datasourceWarning?: boolean;
+  hasCustomLabel?: boolean;
+  label?: string;
+  optionName?: string;
+  // Additional properties that may be passed in
+  metric_name?: string;
+  expression?: string;
+  error_text?: string;
+  uuid?: string;
+  [key: string]: unknown;
+}
+
+function inferSqlExpressionColumn(
+  adhocMetric: AdhocMetricInput,
+): string | null {
   if (
     adhocMetric.sqlExpression &&
     sqlaAutoGeneratedMetricRegex.test(adhocMetric.sqlExpression)
@@ -45,7 +72,9 @@ function inferSqlExpressionColumn(adhocMetric) {
   return null;
 }
 
-function inferSqlExpressionAggregate(adhocMetric) {
+function inferSqlExpressionAggregate(
+  adhocMetric: AdhocMetricInput,
+): string | null {
   if (
     adhocMetric.sqlExpression &&
     sqlaAutoGeneratedMetricRegex.test(adhocMetric.sqlExpression)
@@ -58,15 +87,51 @@ function inferSqlExpressionAggregate(adhocMetric) {
   return null;
 }
 
+/**
+ * Adapter function to create an AdhocMetric instance from a core AdhocMetric type.
+ * This bridges the type gap between @superset-ui/core's AdhocMetric and the local class.
+ */
+export function fromCoreAdhocMetric(metric: CoreAdhocMetric): AdhocMetric {
+  return new AdhocMetric(metric as AdhocMetricInput);
+}
+
+/**
+ * Type guard to check if an object can be used to construct an AdhocMetric.
+ * Returns true for plain objects that have metric-like properties.
+ */
+export function isDictionaryForAdhocMetric(
+  value: unknown,
+): value is AdhocMetricInput {
+  return (
+    typeof value === 'object' &&
+    value !== null &&
+    !(value instanceof AdhocMetric) &&
+    ('expressionType' in value ||
+      'column' in value ||
+      'aggregate' in value ||
+      'sqlExpression' in value ||
+      'metric_name' in value)
+  );
+}
+
 export default class AdhocMetric {
-  constructor(adhocMetric) {
+  expressionType: string;
+  column?: ColumnType | null;
+  aggregate?: string | null;
+  sqlExpression?: string | null;
+  datasourceWarning: boolean;
+  hasCustomLabel: boolean;
+  label: string;
+  optionName: string;
+
+  constructor(adhocMetric: AdhocMetricInput) {
     this.expressionType = adhocMetric.expressionType || EXPRESSION_TYPES.SIMPLE;
     if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
       // try to be clever in the case of transitioning from Sql expression back to simple expression
       const inferredColumn = inferSqlExpressionColumn(adhocMetric);
       this.column =
-        adhocMetric.column ||
-        (inferredColumn && { column_name: inferredColumn });
+        adhocMetric.column ??
+        (inferredColumn ? { column_name: inferredColumn } : null);
       this.aggregate =
         adhocMetric.aggregate || inferSqlExpressionAggregate(adhocMetric);
       this.sqlExpression = null;
@@ -78,7 +143,7 @@ export default class AdhocMetric {
     this.datasourceWarning = !!adhocMetric.datasourceWarning;
     this.hasCustomLabel = !!(adhocMetric.hasCustomLabel && adhocMetric.label);
     this.label = this.hasCustomLabel
-      ? adhocMetric.label
+      ? (adhocMetric.label ?? this.getDefaultLabel())
       : this.getDefaultLabel();
 
     this.optionName =
@@ -88,13 +153,16 @@ export default class AdhocMetric {
         .substring(2, 15)}`;
   }
 
-  getDefaultLabel() {
+  getDefaultLabel(): string {
     return this.translateToSql({ useVerboseName: true });
   }
 
   translateToSql(
-    params = { useVerboseName: false, transformCountDistinct: false },
-  ) {
+    params: { useVerboseName?: boolean; transformCountDistinct?: boolean } = {
+      useVerboseName: false,
+      transformCountDistinct: false,
+    },
+  ): string {
     if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
       const aggregate = this.aggregate || '';
       // eslint-disable-next-line camelcase
@@ -115,19 +183,19 @@ export default class AdhocMetric {
       return aggregate + column;
     }
     if (this.expressionType === EXPRESSION_TYPES.SQL) {
-      return this.sqlExpression;
+      return this.sqlExpression ?? '';
     }
     return '';
   }
 
-  duplicateWith(nextFields) {
+  duplicateWith(nextFields: Partial<AdhocMetricInput>): AdhocMetric {
     return new AdhocMetric({
       ...this,
       ...nextFields,
     });
   }
 
-  equals(adhocMetric) {
+  equals(adhocMetric: AdhocMetric): boolean {
     return (
       adhocMetric.label === this.label &&
       adhocMetric.expressionType === this.expressionType &&
@@ -138,7 +206,7 @@ export default class AdhocMetric {
     );
   }
 
-  isValid() {
+  isValid(): boolean {
     if (this.expressionType === EXPRESSION_TYPES.SIMPLE) {
       return !!(this.column && this.aggregate);
     }
@@ -148,11 +216,11 @@ export default class AdhocMetric {
     return false;
   }
 
-  inferSqlExpressionAggregate() {
-    return inferSqlExpressionAggregate(this);
+  inferSqlExpressionAggregate(): string | null {
+    return inferSqlExpressionAggregate(this as unknown as AdhocMetricInput);
   }
 
-  inferSqlExpressionColumn() {
-    return inferSqlExpressionColumn(this);
+  inferSqlExpressionColumn(): string | null {
+    return inferSqlExpressionColumn(this as unknown as AdhocMetricInput);
   }
 }
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
similarity index 79%
rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
index 3ae9b7b035..2690c70e32 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
@@ -49,6 +49,60 @@ import {
 } from 'src/explore/components/optionRenderers';
 import { getColumnKeywords } from 'src/explore/controlUtils/getColumnKeywords';
 import SQLEditorWithValidation from 'src/components/SQLEditorWithValidation';
+import type { RefObject } from 'react';
+
+interface ColumnType {
+  column_name: string;
+  verbose_name?: string;
+  [key: string]: unknown;
+}
+
+interface SavedMetricType {
+  metric_name: string;
+  verbose_name?: string;
+  expression?: string;
+  [key: string]: unknown;
+}
+
+interface DatasourceInfo {
+  type?: DatasourceType | string;
+  id?: number | string;
+  extra?: string;
+  [key: string]: unknown;
+}
+
+interface ExtraConfig {
+  disallow_adhoc_metrics?: boolean;
+  [key: string]: unknown;
+}
+
+type Metric = AdhocMetric | SavedMetricType;
+
+interface AdhocMetricEditPopoverProps {
+  onChange: (newMetric: Metric, oldMetric?: Metric) => void;
+  onClose: () => void;
+  onResize: () => void;
+  getCurrentTab?: (tab: string) => void;
+  getCurrentLabel?: (labels: {
+    savedMetricLabel?: string;
+    adhocMetricLabel?: string;
+  }) => void;
+  handleDatasetModal?: (open: boolean) => void;
+  adhocMetric: AdhocMetric;
+  columns?: ColumnType[];
+  savedMetricsOptions?: SavedMetricType[];
+  savedMetric?: SavedMetricType;
+  datasource?: DatasourceInfo;
+  isNewMetric?: boolean;
+  isLabelModified?: boolean;
+}
+
+interface AdhocMetricEditPopoverState {
+  adhocMetric: AdhocMetric;
+  savedMetric?: SavedMetricType;
+  width: number;
+  height: number;
+}
 
 const propTypes = {
   onChange: PropTypes.func.isRequired,
@@ -85,11 +139,24 @@ const StyledSelect = styled(Select)`
 
 export const SAVED_TAB_KEY = 'SAVED';
 
-export default class AdhocMetricEditPopover extends PureComponent {
+export default class AdhocMetricEditPopover extends PureComponent<
+  AdhocMetricEditPopoverProps,
+  AdhocMetricEditPopoverState
+> {
   // "Saved" is a default tab unless there are no saved metrics for dataset
   defaultActiveTabKey = this.getDefaultTab();
 
-  constructor(props) {
+  aceEditorRef: RefObject<HTMLDivElement>;
+
+  dragStartX = 0;
+
+  dragStartY = 0;
+
+  dragStartWidth = 0;
+
+  dragStartHeight = 0;
+
+  constructor(props: AdhocMetricEditPopoverProps) {
     super(props);
     this.onSave = this.onSave.bind(this);
     this.onResetStateAndClose = this.onResetStateAndClose.bind(this);
@@ -115,10 +182,13 @@ export default class AdhocMetricEditPopover extends PureComponent {
   }
 
   componentDidMount() {
-    this.props.getCurrentTab(this.defaultActiveTabKey);
+    this.props.getCurrentTab?.(this.defaultActiveTabKey);
   }
 
-  componentDidUpdate(prevProps, prevState) {
+  componentDidUpdate(
+    _prevProps: AdhocMetricEditPopoverProps,
+    prevState: AdhocMetricEditPopoverState,
+  ) {
     if (
       prevState.adhocMetric?.sqlExpression !==
         this.state.adhocMetric?.sqlExpression ||
@@ -127,7 +197,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
         this.state.adhocMetric?.column?.column_name ||
       prevState.savedMetric?.metric_name !== this.state.savedMetric?.metric_name
     ) {
-      this.props.getCurrentLabel({
+      this.props.getCurrentLabel?.({
         savedMetricLabel:
           this.state.savedMetric?.verbose_name ||
           this.state.savedMetric?.metric_name,
@@ -148,7 +218,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
       return adhocMetric.expressionType;
     }
     if (
-      (isNewMetric || savedMetric.metric_name) &&
+      (isNewMetric || savedMetric?.metric_name) &&
       Array.isArray(savedMetricsOptions) &&
       savedMetricsOptions.length > 0
     ) {
@@ -167,8 +237,8 @@ export default class AdhocMetricEditPopover extends PureComponent {
     this.props.onChange(
       {
         ...metric,
-      },
-      oldMetric,
+      } as Metric,
+      oldMetric as Metric,
     );
     this.props.onClose();
   }
@@ -183,8 +253,8 @@ export default class AdhocMetricEditPopover extends PureComponent {
     );
   }
 
-  onColumnChange(columnName) {
-    const column = this.props.columns.find(
+  onColumnChange(columnName: string): void {
+    const column = this.props.columns?.find(
       column => column.column_name === columnName,
     );
     this.setState(prevState => ({
@@ -196,7 +266,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
     }));
   }
 
-  onAggregateChange(aggregate) {
+  onAggregateChange(aggregate: string | null): void {
     // we construct this object explicitly to overwrite the value in the case aggregate is null
     this.setState(prevState => ({
       adhocMetric: prevState.adhocMetric.duplicateWith({
@@ -207,8 +277,8 @@ export default class AdhocMetricEditPopover extends PureComponent {
     }));
   }
 
-  onSavedMetricChange(savedMetricName) {
-    const savedMetric = this.props.savedMetricsOptions.find(
+  onSavedMetricChange(savedMetricName: string): void {
+    const savedMetric = this.props.savedMetricsOptions?.find(
       metric => metric.metric_name === savedMetricName,
     );
     this.setState(prevState => ({
@@ -222,7 +292,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
     }));
   }
 
-  onSqlExpressionChange(sqlExpression) {
+  onSqlExpressionChange(sqlExpression: string): void {
     this.setState(prevState => ({
       adhocMetric: prevState.adhocMetric.duplicateWith({
         sqlExpression,
@@ -232,7 +302,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
     }));
   }
 
-  onDragDown(e) {
+  onDragDown(e: React.MouseEvent): void {
     this.dragStartX = e.clientX;
     this.dragStartY = e.clientY;
     this.dragStartWidth = this.state.width;
@@ -240,7 +310,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
     document.addEventListener('mousemove', this.onMouseMove);
   }
 
-  onMouseMove(e) {
+  onMouseMove(e: MouseEvent): void {
     this.props.onResize();
     this.setState({
       width: Math.max(
@@ -254,32 +324,42 @@ export default class AdhocMetricEditPopover extends PureComponent {
     });
   }
 
-  onMouseUp() {
+  onMouseUp(): void {
     document.removeEventListener('mousemove', this.onMouseMove);
   }
 
-  onTabChange(tab) {
+  onTabChange(tab: string): void {
     this.refreshAceEditor();
-    this.props.getCurrentTab(tab);
+    this.props.getCurrentTab?.(tab);
   }
 
-  refreshAceEditor() {
+  refreshAceEditor(): void {
     setTimeout(() => {
       if (this.aceEditorRef.current) {
-        this.aceEditorRef.current.editor?.resize?.();
+        // Cast to access ace editor API
+        (
+          this.aceEditorRef.current as unknown as {
+            editor?: { resize?: () => void };
+          }
+        ).editor?.resize?.();
       }
     }, 0);
   }
 
-  renderColumnOption(option) {
+  renderColumnOption(option: ColumnType): React.ReactNode {
     const column = { ...option };
-    if (column.metric_name && !column.verbose_name) {
-      column.verbose_name = column.metric_name;
+    if (
+      (column as unknown as { metric_name?: string }).metric_name &&
+      !column.verbose_name
+    ) {
+      column.verbose_name = (
+        column as unknown as { metric_name: string }
+      ).metric_name;
     }
     return <StyledColumnOption column={column} showType />;
   }
 
-  renderMetricOption(savedMetric) {
+  renderMetricOption(savedMetric: SavedMetricType): React.ReactNode {
     return <StyledMetricOption metric={savedMetric} showType />;
   }
 
@@ -298,7 +378,12 @@ export default class AdhocMetricEditPopover extends PureComponent {
       ...popoverProps
     } = this.props;
     const { adhocMetric, savedMetric } = this.state;
-    const keywords = sqlKeywords.concat(getColumnKeywords(columns));
+    const columnsArray = columns ?? [];
+    const keywords = sqlKeywords.concat(
+      getColumnKeywords(
+        columnsArray as Parameters<typeof getColumnKeywords>[0],
+      ),
+    );
 
     const columnValue =
       (adhocMetric.column && adhocMetric.column.column_name) ||
@@ -307,7 +392,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
     // autofocus on column if there's no value in column; otherwise autofocus on aggregate
     const columnSelectProps = {
       ariaLabel: t('Select column'),
-      placeholder: t('%s column(s)', columns.length),
+      placeholder: t('%s column(s)', columnsArray.length),
       value: columnValue,
       onChange: this.onColumnChange,
       allowClear: true,
@@ -317,8 +402,11 @@ export default class AdhocMetricEditPopover extends PureComponent {
     const aggregateSelectProps = {
       ariaLabel: t('Select aggregate options'),
       placeholder: t('%s aggregates(s)', AGGREGATES_OPTIONS.length),
-      value: adhocMetric.aggregate || adhocMetric.inferSqlExpressionAggregate(),
-      onChange: this.onAggregateChange,
+      value:
+        adhocMetric.aggregate ??
+        adhocMetric.inferSqlExpressionAggregate() ??
+        undefined,
+      onChange: this.onAggregateChange as (value: unknown) => void,
       allowClear: true,
       autoFocus: !!columnValue,
     };
@@ -343,10 +431,10 @@ export default class AdhocMetricEditPopover extends PureComponent {
       ) &&
         savedMetric?.metric_name !== propsSavedMetric?.metric_name);
 
-    let extra = {};
-    if (datasource?.extra) {
+    let extra: ExtraConfig = {};
+    if (datasource?.extra && typeof datasource.extra === 'string') {
       try {
-        extra = JSON.parse(datasource.extra);
+        extra = JSON.parse(datasource.extra) as ExtraConfig;
       } catch {} // eslint-disable-line no-empty
     }
 
@@ -383,7 +471,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
                       {...savedSelectProps}
                     />
                   </FormItem>
-                ) : datasource.type === DatasourceType.Table ? (
+                ) : datasource?.type === DatasourceType.Table ? (
                   <EmptyState
                     image="empty.svg"
                     size="small"
@@ -403,7 +491,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
                           tabIndex={0}
                           role="button"
                           onClick={() => {
-                            this.props.handleDatasetModal(true);
+                            this.props.handleDatasetModal?.(true);
                             this.props.onClose();
                           }}
                         >
@@ -433,9 +521,9 @@ export default class AdhocMetricEditPopover extends PureComponent {
                 <>
                   <FormItem label={t('column')}>
                     <Select
-                      options={columns.map(column => ({
+                      options={columnsArray.map(column => ({
                         value: column.column_name,
-                        key: column.id,
+                        key: (column as { id?: unknown }).id,
                         label: this.renderColumnOption(column),
                       }))}
                       {...columnSelectProps}
@@ -527,5 +615,7 @@ export default class AdhocMetricEditPopover extends PureComponent {
     );
   }
 }
+// @ts-expect-error - propTypes are defined for runtime validation but TypeScript handles type checking
 AdhocMetricEditPopover.propTypes = propTypes;
+// @ts-expect-error - defaultProps for backward compatibility with PropTypes
 AdhocMetricEditPopover.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.tsx
similarity index 90%
rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.tsx
index e30d0c0b81..72a7c975b9 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.tsx
@@ -37,41 +37,40 @@ const sumValueAdhocMetric = new AdhocMetric({
   aggregate: AGGREGATES.SUM,
 });
 
-const datasource = {
-  type: 'table',
-  id: 1,
-  uid: '1__table',
-  columnFormats: {},
-  verboseMap: {},
-};
-
 const defaultProps = {
   adhocMetric: sumValueAdhocMetric,
   savedMetric: {},
   savedMetricsOptions: [],
   onMetricEdit: jest.fn(),
   columns,
-  datasource,
+  datasource: {
+    type: 'table',
+    id: 1,
+    uid: '1__table',
+    columnFormats: {},
+    verboseMap: {},
+  },
   onMoveLabel: jest.fn(),
   onDropLabel: jest.fn(),
   index: 0,
 };
 
-function setup(overrides) {
+function setup(overrides: Record<string, unknown> = {}) {
   const props = {
     ...defaultProps,
     ...overrides,
   };
-  return render(<AdhocMetricOption {...props} />, { useDnd: true });
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return render(<AdhocMetricOption {...(props as any)} />, { useDnd: true });
 }
 
 test('renders an overlay trigger wrapper for the label', () => {
-  setup();
+  setup({});
   expect(screen.getByText('SUM(value)')).toBeInTheDocument();
 });
 
 test('overwrites the adhocMetric in state with onLabelChange', async () => {
-  setup();
+  setup({});
   userEvent.click(screen.getByText('SUM(value)'));
   userEvent.click(screen.getByTestId(/AdhocMetricEditTitle#trigger/i));
   const labelInput = await screen.findByTestId(/AdhocMetricEditTitle#input/i);
@@ -86,7 +85,7 @@ test('overwrites the adhocMetric in state with onLabelChange', async () => {
 });
 
 test('returns to default labels when the custom label is cleared', async () => {
-  setup();
+  setup({});
   userEvent.click(screen.getByText('SUM(value)'));
   userEvent.click(screen.getByTestId(/AdhocMetricEditTitle#trigger/i));
   const labelInput = await screen.findByTestId(/AdhocMetricEditTitle#input/i);
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx
similarity index 63%
rename from superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx
index 7ca0f7065f..34a016e986 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx
@@ -18,12 +18,32 @@
  */
 import { PureComponent } from 'react';
 import PropTypes from 'prop-types';
+import { Metric } from '@superset-ui/core';
 import { OptionControlLabel } from 'src/explore/components/controls/OptionControls';
 import { DndItemType } from 'src/explore/components/DndItemType';
+import { Datasource } from 'src/explore/types';
+import { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal';
 import columnType from './columnType';
 import AdhocMetric from './AdhocMetric';
 import savedMetricType from './savedMetricType';
 import AdhocMetricPopoverTrigger from './AdhocMetricPopoverTrigger';
+import { savedMetricType as SavedMetricTypeDef } from './types';
+
+interface AdhocMetricOptionProps {
+  adhocMetric: AdhocMetric;
+  onMetricEdit: (newMetric: Metric, oldMetric: Metric) => void;
+  onRemoveMetric?: (index: number) => void;
+  columns?: { column_name: string; type: string }[];
+  savedMetricsOptions?: SavedMetricTypeDef[];
+  savedMetric?: SavedMetricTypeDef | Record<string, never>;
+  datasource?: Datasource & ISaveableDatasource;
+  onMoveLabel?: (dragIndex: number, hoverIndex: number) => void;
+  onDropLabel?: () => void;
+  index?: number;
+  type?: string;
+  multi?: boolean;
+  datasourceWarningMessage?: string;
+}
 
 const propTypes = {
   adhocMetric: PropTypes.instanceOf(AdhocMetric),
@@ -41,15 +61,15 @@ const propTypes = {
   datasourceWarningMessage: PropTypes.string,
 };
 
-class AdhocMetricOption extends PureComponent {
-  constructor(props) {
+class AdhocMetricOption extends PureComponent<AdhocMetricOptionProps> {
+  constructor(props: AdhocMetricOptionProps) {
     super(props);
     this.onRemoveMetric = this.onRemoveMetric.bind(this);
   }
 
-  onRemoveMetric(e) {
+  onRemoveMetric(e?: React.MouseEvent): void {
     e?.stopPropagation();
-    this.props.onRemoveMetric(this.props.index);
+    this.props.onRemoveMetric?.(this.props.index ?? 0);
   }
 
   render() {
@@ -58,7 +78,7 @@ class AdhocMetricOption extends PureComponent {
       onMetricEdit,
       columns,
       savedMetricsOptions,
-      savedMetric,
+      savedMetric = {} as SavedMetricTypeDef,
       datasource,
       onMoveLabel,
       onDropLabel,
@@ -67,25 +87,26 @@ class AdhocMetricOption extends PureComponent {
       multi,
       datasourceWarningMessage,
     } = this.props;
-    const withCaret = !savedMetric.error_text;
+    const withCaret = !(savedMetric as SavedMetricTypeDef).error_text;
 
     return (
       <AdhocMetricPopoverTrigger
         adhocMetric={adhocMetric}
         onMetricEdit={onMetricEdit}
-        columns={columns}
-        savedMetricsOptions={savedMetricsOptions}
+        columns={columns ?? []}
+        savedMetricsOptions={savedMetricsOptions ?? []}
         savedMetric={savedMetric}
-        datasource={datasource}
+        datasource={datasource!}
       >
         <OptionControlLabel
-          savedMetric={savedMetric}
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          savedMetric={savedMetric as any}
           adhocMetric={adhocMetric}
           label={adhocMetric.label}
-          onRemove={this.onRemoveMetric}
+          onRemove={() => this.onRemoveMetric()}
           onMoveLabel={onMoveLabel}
           onDropLabel={onDropLabel}
-          index={index}
+          index={index ?? 0}
           type={type ?? DndItemType.AdhocMetricOption}
           withCaret={withCaret}
           isFunction
@@ -99,4 +120,5 @@ class AdhocMetricOption extends PureComponent {
 
 export default AdhocMetricOption;
 
+// @ts-expect-error - propTypes are defined for runtime validation but TypeScript handles type checking
 AdhocMetricOption.propTypes = propTypes;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
index 5e59e4a5c3..95580a742f 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger.tsx
@@ -37,7 +37,7 @@ export type AdhocMetricPopoverTriggerProps = {
   onMetricEdit(newMetric: Metric, oldMetric: Metric): void;
   columns: { column_name: string; type: string }[];
   savedMetricsOptions: savedMetricType[];
-  savedMetric: savedMetricType;
+  savedMetric: savedMetricType | Record<string, never>;
   datasource: Datasource & ISaveableDatasource;
   children: ReactNode;
   isControlledComponent?: boolean;
@@ -201,8 +201,8 @@ class AdhocMetricPopoverTrigger extends PureComponent<
     const { visible, togglePopover, closePopover } = isControlledComponent
       ? {
           visible: this.props.visible,
-          togglePopover: this.props.togglePopover,
-          closePopover: this.props.closePopover,
+          togglePopover: this.props.togglePopover ?? this.togglePopover,
+          closePopover: this.props.closePopover ?? this.closePopover,
         }
       : {
           visible: this.state.popoverVisible,
@@ -216,12 +216,20 @@ class AdhocMetricPopoverTrigger extends PureComponent<
           adhocMetric={adhocMetric}
           columns={columns}
           savedMetricsOptions={savedMetricsOptions}
-          savedMetric={savedMetric}
-          datasource={datasource}
+          savedMetric={savedMetric as savedMetricType}
+          datasource={
+            datasource as unknown as {
+              type?: string;
+              id?: number | string;
+              extra?: string;
+            }
+          }
           handleDatasetModal={this.handleDatasetModal}
           onResize={this.onPopoverResize}
           onClose={closePopover}
-          onChange={this.onChange}
+          onChange={
+            this.onChange as (newMetric: unknown, oldMetric?: unknown) => void
+          }
           getCurrentTab={this.getCurrentTab}
           getCurrentLabel={this.getCurrentLabel}
           isNewMetric={this.props.isNew}
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.tsx
similarity index 94%
rename from superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.tsx
index 96eb9d46eb..dee87d5a2a 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.tsx
@@ -44,7 +44,11 @@ describe('FilterDefinitionOption', () => {
   });
 
   test('renders a StyledColumnOption given an adhoc metric', async () => {
-    render(<FilterDefinitionOption option={sumValueAdhocMetric} />);
+    render(
+      <FilterDefinitionOption
+        option={sumValueAdhocMetric as unknown as { label: string }}
+      />,
+    );
     await expect(screen.getByText('SUM(source)')).toBeVisible();
   });
 
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
similarity index 81%
rename from superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
index f310d212e6..c8aaf48855 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
@@ -22,6 +22,14 @@ import columnType from './columnType';
 import adhocMetricType from './adhocMetricType';
 import { StyledColumnOption } from '../../optionRenderers';
 
+interface OptionType {
+  saved_metric_name?: string;
+  column_name?: string;
+  label?: string;
+  type?: string;
+  [key: string]: unknown;
+}
+
 const propTypes = {
   option: PropTypes.oneOfType([
     columnType,
@@ -30,7 +38,11 @@ const propTypes = {
   ]).isRequired,
 };
 
-export default function FilterDefinitionOption({ option }) {
+export default function FilterDefinitionOption({
+  option,
+}: {
+  option: OptionType;
+}) {
   if (option.saved_metric_name) {
     return (
       <StyledColumnOption
@@ -40,7 +52,12 @@ export default function FilterDefinitionOption({ option }) {
     );
   }
   if (option.column_name) {
-    return <StyledColumnOption column={option} showType />;
+    return (
+      <StyledColumnOption
+        column={option as { column_name: string; type?: string }}
+        showType
+      />
+    );
   }
   if (option.label) {
     return (
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.tsx
similarity index 83%
rename from superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.tsx
index d55bfa1d83..51717d0d57 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.tsx
@@ -26,17 +26,21 @@ const sumValueAdhocMetric = new AdhocMetric({
   aggregate: AGGREGATES.SUM,
 });
 
-const setup = propOverrides => {
+const defaultProps = {
+  onMetricEdit: jest.fn(),
+  option: sumValueAdhocMetric as AdhocMetric,
+  index: 1,
+  columns: [],
+  savedMetrics: [],
+  savedMetricsOptions: [],
+  datasource: undefined,
+  onMoveLabel: jest.fn(),
+  onDropLabel: jest.fn(),
+};
+
+const setup = (propOverrides: Record<string, unknown> = {}) => {
   const props = {
-    onMetricEdit: jest.fn(),
-    option: sumValueAdhocMetric,
-    index: 1,
-    columns: [],
-    savedMetrics: [],
-    savedMetricsOptions: [],
-    datasource: {},
-    onMoveLabel: jest.fn(),
-    onDropLabel: jest.fn(),
+    ...defaultProps,
     ...propOverrides,
   };
   return render(<MetricDefinitionValue {...props} />, { useDnd: true });
@@ -50,6 +54,6 @@ test('renders a MetricOption given a saved metric', () => {
 });
 
 test('renders an AdhocMetricOption given an adhoc metric', () => {
-  setup();
+  setup({});
   expect(screen.getByText('SUM(value)')).toBeInTheDocument();
 });
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx
similarity index 67%
rename from superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx
index c300252dd2..4ad35b8d67 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx
@@ -17,10 +17,30 @@
  * under the License.
  */
 import PropTypes from 'prop-types';
+import { Metric } from '@superset-ui/core';
+import { Datasource } from 'src/explore/types';
+import { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal';
 import columnType from './columnType';
 import AdhocMetricOption from './AdhocMetricOption';
 import AdhocMetric from './AdhocMetric';
 import savedMetricType from './savedMetricType';
+import { savedMetricType as SavedMetricTypeDef } from './types';
+
+interface MetricDefinitionValueProps {
+  option: AdhocMetric | SavedMetricTypeDef | string;
+  index: number;
+  onMetricEdit?: (newMetric: Metric, oldMetric: Metric) => void;
+  onRemoveMetric?: (index: number) => void;
+  onMoveLabel?: (dragIndex: number, hoverIndex: number) => void;
+  onDropLabel?: () => void;
+  columns?: { column_name: string; type: string }[];
+  savedMetrics?: SavedMetricTypeDef[];
+  savedMetricsOptions?: SavedMetricTypeDef[];
+  multi?: boolean;
+  datasource?: Datasource & ISaveableDatasource;
+  datasourceWarningMessage?: string;
+  type?: string;
+}
 
 const propTypes = {
   option: PropTypes.oneOfType([PropTypes.object, PropTypes.string]).isRequired,
@@ -51,14 +71,14 @@ export default function MetricDefinitionValue({
   type,
   multi,
   datasourceWarningMessage,
-}) {
-  const getSavedMetricByName = metricName =>
-    savedMetrics.find(metric => metric.metric_name === metricName);
+}: MetricDefinitionValueProps) {
+  const getSavedMetricByName = (metricName: string) =>
+    savedMetrics?.find(metric => metric.metric_name === metricName);
 
   let savedMetric;
   if (typeof option === 'string') {
     savedMetric = getSavedMetricByName(option);
-  } else if (option.metric_name) {
+  } else if ((option as SavedMetricTypeDef).metric_name) {
     savedMetric = option;
   }
 
@@ -82,7 +102,8 @@ export default function MetricDefinitionValue({
       datasourceWarningMessage,
     };
 
-    return <AdhocMetricOption {...metricOptionProps} />;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    return <AdhocMetricOption {...(metricOptionProps as any)} />;
   }
   return null;
 }
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.tsx
similarity index 92%
rename from superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.tsx
index e54ff242da..234b5068c3 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.tsx
@@ -51,10 +51,11 @@ const defaultProps = {
     { metric_name: 'sum__value', expression: 'SUM(energy_usage.value)' },
     { metric_name: 'avg__value', expression: 'AVG(energy_usage.value)' },
   ],
+  datasource: undefined,
   datasourceType: 'sqla',
 };
 
-function setup(overrides) {
+function setup(overrides: Record<string, unknown> = {}) {
   const onChange = jest.fn();
   const props = {
     onChange,
@@ -92,7 +93,7 @@ test('handles creating a new metric', async () => {
   const { onChange } = setup();
 
   userEvent.click(screen.getByText(/add metric/i));
-  await selectOption('sum__value', /select saved metrics/i);
+  await selectOption('sum__value', 'Select saved metrics');
   userEvent.click(screen.getByRole('button', { name: /save/i }));
   expect(onChange).toHaveBeenCalledWith(['sum__value']);
 });
@@ -106,7 +107,7 @@ test('accepts an edited metric from an AdhocMetricEditPopover', async () => {
   userEvent.click(metricLabel);
 
   await screen.findByText('aggregate');
-  selectOption('AVG', /select aggregate options/i);
+  selectOption('AVG', 'Select aggregate options');
 
   await screen.findByText('AVG(value)');
 
@@ -130,7 +131,7 @@ test('removes metrics if savedMetrics changes', async () => {
 
   const savedTab = screen.getByRole('tab', { name: /saved/i });
   userEvent.click(savedTab);
-  await selectOption('avg__value', /select saved metrics/i);
+  await selectOption('avg__value', 'Select saved metrics');
 
   const simpleTab = screen.getByRole('tab', { name: /simple/i });
   userEvent.click(simpleTab);
@@ -143,6 +144,9 @@ test('removes metrics if savedMetrics changes', async () => {
 test('does not remove custom SQL metric if savedMetrics changes', async () => {
   const { rerender } = render(
     <MetricsControl
+      name="metrics"
+      onChange={jest.fn()}
+      multi
       value={[
         {
           expressionType: EXPRESSION_TYPES.SQL,
@@ -160,6 +164,7 @@ test('does not remove custom SQL metric if savedMetrics changes', async () => {
         { metric_name: 'sum__value', expression: 'SUM(energy_usage.value)' },
         { metric_name: 'avg__value', expression: 'AVG(energy_usage.value)' },
       ]}
+      datasource={undefined}
     />,
     { useDnd: true },
   );
@@ -169,6 +174,9 @@ test('does not remove custom SQL metric if savedMetrics changes', async () => {
   // Simulate removing columns
   rerender(
     <MetricsControl
+      name="metrics"
+      onChange={jest.fn()}
+      multi
       value={[
         {
           expressionType: EXPRESSION_TYPES.SQL,
@@ -179,6 +187,7 @@ test('does not remove custom SQL metric if savedMetrics changes', async () => {
       ]}
       columns={[]}
       savedMetrics={[]}
+      datasource={undefined}
     />,
   );
 
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx
similarity index 76%
rename from superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
rename to superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx
index 082befda40..c9c3fd6b29 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
+++ b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import PropTypes from 'prop-types';
 import { ensureIsArray, t, usePrevious } from '@superset-ui/core';
 import { isEqual } from 'lodash';
@@ -57,13 +57,14 @@ const defaultProps = {
   columns: [],
 };
 
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
 function getOptionsForSavedMetrics(
-  savedMetrics,
-  currentMetricValues,
-  currentMetric,
+  savedMetrics: any,
+  currentMetricValues: any,
+  currentMetric: any,
 ) {
   return (
-    savedMetrics?.filter(savedMetric =>
+    savedMetrics?.filter((savedMetric: { metric_name: string }) =>
       Array.isArray(currentMetricValues)
         ? !currentMetricValues.includes(savedMetric.metric_name) ||
           savedMetric.metric_name === currentMetric
@@ -72,13 +73,15 @@ function getOptionsForSavedMetrics(
   );
 }
 
-function isDictionaryForAdhocMetric(value) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function isDictionaryForAdhocMetric(value: any) {
   return value && !(value instanceof AdhocMetric) && value.expressionType;
 }
 
 // adhoc metrics are stored as dictionaries in URL params. We convert them back into the
 // AdhocMetric class for typechecking, consistency and instance method access.
-function coerceAdhocMetrics(value) {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function coerceAdhocMetrics(value: any) {
   if (!value) {
     return [];
   }
@@ -88,7 +91,8 @@ function coerceAdhocMetrics(value) {
     }
     return [value];
   }
-  return value.map(val => {
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  return value.map((val: any) => {
     if (isDictionaryForAdhocMetric(val)) {
       return new AdhocMetric(val);
     }
@@ -99,21 +103,42 @@ function coerceAdhocMetrics(value) {
 const emptySavedMetric = { metric_name: '', expression: '' };
 
 // TODO: use typeguards to distinguish saved metrics from adhoc metrics
-const getMetricsMatchingCurrentDataset = (value, columns, savedMetrics) =>
-  ensureIsArray(value).filter(metric => {
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const getMetricsMatchingCurrentDataset = (
+  value: any,
+  columns: any,
+  savedMetrics: any,
+) =>
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  ensureIsArray(value).filter((metric: any) => {
     if (typeof metric === 'string' || metric.metric_name) {
       return savedMetrics?.some(
-        savedMetric =>
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        (savedMetric: any) =>
           savedMetric.metric_name === metric ||
           savedMetric.metric_name === metric.metric_name,
       );
     }
     return columns?.some(
-      column =>
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      (column: any) =>
         !metric.column || metric.column.column_name === column.column_name,
     );
   });
 
+interface MetricsControlProps {
+  name: string;
+  onChange: (value: unknown) => void;
+  multi?: boolean;
+  value?: unknown;
+  columns?: unknown[];
+  savedMetrics?: unknown[];
+  datasource?: unknown;
+  clearable?: boolean;
+  isLoading?: boolean;
+  [key: string]: unknown;
+}
+
 const MetricsControl = ({
   onChange,
   multi,
@@ -122,13 +147,14 @@ const MetricsControl = ({
   savedMetrics,
   datasource,
   ...props
-}) => {
+}: MetricsControlProps) => {
   const [value, setValue] = useState(coerceAdhocMetrics(propsValue));
   const prevColumns = usePrevious(columns);
   const prevSavedMetrics = usePrevious(savedMetrics);
 
   const handleChange = useCallback(
-    opts => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    (opts: any) => {
       // if clear out options
       if (opts === null) {
         onChange(null);
@@ -137,21 +163,22 @@ const MetricsControl = ({
 
       const transformedOpts = ensureIsArray(opts);
       const optionValues = transformedOpts
-        .map(option => {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        .map((option: any) => {
           // pre-defined metric
           if (option.metric_name) {
             return option.metric_name;
           }
           return option;
         })
-        .filter(option => option);
+        .filter((option: unknown) => option);
       onChange(multi ? optionValues : optionValues[0]);
     },
     [multi, onChange],
   );
 
   const onNewMetric = useCallback(
-    newMetric => {
+    (newMetric: unknown) => {
       const newValue = [...value, newMetric];
       setValue(newValue);
       handleChange(newValue);
@@ -160,8 +187,10 @@ const MetricsControl = ({
   );
 
   const onMetricEdit = useCallback(
-    (changedMetric, oldMetric) => {
-      const newValue = value.map(val => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    (changedMetric: any, oldMetric: any) => {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const newValue = value.map((val: any) => {
         if (
           // compare saved metrics
           val === oldMetric.metric_name ||
@@ -181,7 +210,7 @@ const MetricsControl = ({
   );
 
   const onRemoveMetric = useCallback(
-    index => {
+    (index: number) => {
       if (!Array.isArray(value)) {
         return;
       }
@@ -194,7 +223,7 @@ const MetricsControl = ({
   );
 
   const moveLabel = useCallback(
-    (dragIndex, hoverIndex) => {
+    (dragIndex: number, hoverIndex: number) => {
       const newValues = [...value];
       [newValues[hoverIndex], newValues[dragIndex]] = [
         newValues[dragIndex],
@@ -217,7 +246,7 @@ const MetricsControl = ({
 
   const newAdhocMetric = useMemo(() => new AdhocMetric({}), [value]);
   const addNewMetricPopoverTrigger = useCallback(
-    trigger => {
+    (trigger: React.ReactNode) => {
       if (isAddNewMetricDisabled()) {
         return trigger;
       }
@@ -225,10 +254,12 @@ const MetricsControl = ({
         <AdhocMetricPopoverTrigger
           adhocMetric={newAdhocMetric}
           onMetricEdit={onNewMetric}
-          columns={columns}
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          columns={columns as any}
           savedMetricsOptions={savedMetricOptions}
           savedMetric={emptySavedMetric}
-          datasource={datasource}
+          // eslint-disable-next-line @typescript-eslint/no-explicit-any
+          datasource={datasource as any}
           isNew
         >
           {trigger}
@@ -274,16 +305,20 @@ const MetricsControl = ({
   );
 
   const valueRenderer = useCallback(
-    (option, index) => (
+    (option: unknown, index: number) => (
       <MetricDefinitionValue
         key={index}
         index={index}
-        option={option}
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        option={option as any}
         onMetricEdit={onMetricEdit}
         onRemoveMetric={onRemoveMetric}
-        columns={columns}
-        datasource={datasource}
-        savedMetrics={savedMetrics}
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        columns={columns as any}
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        datasource={datasource as any}
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        savedMetrics={savedMetrics as any}
         savedMetricsOptions={getOptionsForSavedMetrics(
           savedMetrics,
           value,
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.js b/superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.ts
similarity index 100%
rename from superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.js
rename to superset-frontend/src/explore/components/controls/MetricControl/adhocMetricType.ts
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/types.ts b/superset-frontend/src/explore/components/controls/MetricControl/types.ts
index 7e16810641..e1f9d7f347 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/MetricControl/types.ts
@@ -20,6 +20,8 @@ export type savedMetricType = {
   metric_name: string;
   verbose_name?: string;
   expression: string;
+  error_text?: string;
+  id?: number | string;
 };
 
 export interface AggregateOption {
diff --git a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx
index 55771bf567..91b574a201 100644
--- a/superset-frontend/src/explore/components/controls/OptionControls/index.tsx
+++ b/superset-frontend/src/explore/components/controls/OptionControls/index.tsx
@@ -259,8 +259,8 @@ export const OptionControlLabel = ({
   savedMetric?: savedMetricType;
   adhocMetric?: AdhocMetric;
   onRemove: () => void;
-  onMoveLabel: (dragIndex: number, hoverIndex: number) => void;
-  onDropLabel: () => void;
+  onMoveLabel?: (dragIndex: number, hoverIndex: number) => void;
+  onDropLabel?: () => void;
   withCaret?: boolean;
   isFunction?: boolean;
   isDraggable?: boolean;
diff --git a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx b/superset-frontend/src/explore/components/controls/SelectControl.test.tsx
similarity index 94%
rename from superset-frontend/src/explore/components/controls/SelectControl.test.jsx
rename to superset-frontend/src/explore/components/controls/SelectControl.test.tsx
index 662895e4af..ce9f2ad28c 100644
--- a/superset-frontend/src/explore/components/controls/SelectControl.test.jsx
+++ b/superset-frontend/src/explore/components/controls/SelectControl.test.tsx
@@ -31,7 +31,13 @@ import SelectControl, {
   getSortComparator,
 } from 'src/explore/components/controls/SelectControl';
 
-const defaultProps = {
+const defaultProps: {
+  choices: [string | number, string][];
+  name: string;
+  label: string;
+  valueKey: string;
+  onChange: jest.Mock;
+} = {
   choices: [
     ['1 year ago', '1 year ago'],
     ['1 week ago', '1 week ago'],
@@ -306,14 +312,19 @@ describe('SelectControl', () => {
 
     test('returns false for empty items', () => {
       expect(areAllValuesNumbers([])).toBe(false);
+      // @ts-expect-error testing invalid input
       expect(areAllValuesNumbers(null)).toBe(false);
+      // @ts-expect-error testing invalid input
       expect(areAllValuesNumbers(undefined)).toBe(false);
     });
   });
 
   // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
   describe('getSortComparator', () => {
-    const mockExplicitComparator = (a, b) => a.label.localeCompare(b.label);
+    const mockExplicitComparator = (
+      a: { label: string },
+      b: { label: string },
+    ) => a.label.localeCompare(b.label);
 
     test('returns explicit comparator when provided', () => {
       const choices = [
@@ -322,7 +333,7 @@ describe('SelectControl', () => {
       ];
       const result = getSortComparator(
         choices,
-        null,
+        undefined,
         'value',
         mockExplicitComparator,
       );
@@ -334,7 +345,7 @@ describe('SelectControl', () => {
         [1, 'One'],
         [2, 'Two'],
       ];
-      const result = getSortComparator(choices, null, 'value', null);
+      const result = getSortComparator(choices, undefined, 'value', undefined);
       expect(typeof result).toBe('function');
       expect(result).not.toBe(mockExplicitComparator);
     });
@@ -344,7 +355,7 @@ describe('SelectControl', () => {
         { value: 1, label: 'One' },
         { value: 2, label: 'Two' },
       ];
-      const result = getSortComparator(null, options, 'value', null);
+      const result = getSortComparator(undefined, options, 'value', undefined);
       expect(typeof result).toBe('function');
       expect(result).not.toBe(mockExplicitComparator);
     });
@@ -358,7 +369,7 @@ describe('SelectControl', () => {
         { value: 3, label: 'Three' },
         { value: 4, label: 'Four' },
       ];
-      const result = getSortComparator(choices, options, 'value', null);
+      const result = getSortComparator(choices, options, 'value', undefined);
       expect(typeof result).toBe('function');
     });
 
@@ -367,7 +378,7 @@ describe('SelectControl', () => {
         ['one', 'One'],
         ['two', 'Two'],
       ];
-      const result = getSortComparator(choices, null, 'value', null);
+      const result = getSortComparator(choices, undefined, 'value', undefined);
       expect(result).toBeUndefined();
     });
 
@@ -376,12 +387,17 @@ describe('SelectControl', () => {
         { value: 'one', label: 'One' },
         { value: 'two', label: 'Two' },
       ];
-      const result = getSortComparator(null, options, 'value', null);
+      const result = getSortComparator(undefined, options, 'value', undefined);
       expect(result).toBeUndefined();
     });
 
     test('returns undefined when no choices or options provided', () => {
-      const result = getSortComparator(null, null, 'value', null);
+      const result = getSortComparator(
+        undefined,
+        undefined,
+        'value',
+        undefined,
+      );
       expect(result).toBeUndefined();
     });
   });
diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.tsx
similarity index 65%
rename from superset-frontend/src/explore/components/controls/SelectControl.jsx
rename to superset-frontend/src/explore/components/controls/SelectControl.tsx
index 5ac1166847..932538ceff 100644
--- a/superset-frontend/src/explore/components/controls/SelectControl.jsx
+++ b/superset-frontend/src/explore/components/controls/SelectControl.tsx
@@ -16,13 +16,61 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { PureComponent } from 'react';
+import { PureComponent, type ReactNode } from 'react';
 import PropTypes from 'prop-types';
 import { isEqualArray, t } from '@superset-ui/core';
 import { css } from '@apache-superset/core/ui';
 import { Select } from '@superset-ui/core/components';
 import ControlHeader from 'src/explore/components/ControlHeader';
 
+type SelectValue = string | number | (string | number)[] | null | undefined;
+
+interface SelectOption {
+  value: string | number;
+  label: string;
+  [key: string]: unknown;
+}
+
+export interface SelectControlProps {
+  ariaLabel?: string;
+  autoFocus?: boolean;
+  choices?: [string | number, string][];
+  clearable?: boolean;
+  description?: string | ReactNode;
+  disabled?: boolean;
+  freeForm?: boolean;
+  isLoading?: boolean;
+  mode?: string;
+  multi?: boolean;
+  isMulti?: boolean;
+  name: string;
+  onChange?: (value: SelectValue, options?: unknown[]) => void;
+  onFocus?: () => void;
+  onSelect?: (value: SelectValue) => void;
+  onDeselect?: (value: SelectValue) => void;
+  value?: SelectValue;
+  default?: SelectValue;
+  showHeader?: boolean;
+  optionRenderer?: (option: unknown) => ReactNode;
+  valueKey?: string;
+  options?: { value: string | number; label: string; [key: string]: unknown }[];
+  placeholder?: string;
+  filterOption?: (input: unknown, option: unknown) => boolean;
+  tokenSeparators?: string[];
+  notFoundContent?: ReactNode;
+  label?: string;
+  renderTrigger?: boolean;
+  validationErrors?: string[];
+  rightNode?: ReactNode;
+  leftNode?: ReactNode;
+  onClick?: () => void;
+  hovered?: boolean;
+  tooltipOnClick?: () => void;
+  warning?: string;
+  danger?: string;
+  sortComparator?: (a: SelectOption, b: SelectOption) => number;
+}
+
 const propTypes = {
   ariaLabel: PropTypes.string,
   autoFocus: PropTypes.bool,
@@ -88,9 +136,17 @@ const defaultProps = {
   valueKey: 'value',
 };
 
-const numberComparator = (a, b) => a.value - b.value;
+interface SelectControlState {
+  options: SelectOption[];
+}
+
+const numberComparator = (a: SelectOption, b: SelectOption): number =>
+  (a.value as number) - (b.value as number);
 
-export const areAllValuesNumbers = (items, valueKey = 'value') => {
+export const areAllValuesNumbers = (
+  items: unknown[],
+  valueKey = 'value',
+): boolean => {
   if (!items || items.length === 0) {
     return false;
   }
@@ -100,18 +156,22 @@ export const areAllValuesNumbers = (items, valueKey = 'value') => {
       return typeof value === 'number';
     }
     if (typeof item === 'object' && item !== null) {
-      return typeof item[valueKey] === 'number';
+      return typeof (item as Record<string, unknown>)[valueKey] === 'number';
     }
     return typeof item === 'number';
   });
 };
 
+type SortComparator =
+  | ((a: SelectOption, b: SelectOption) => number)
+  | undefined;
+
 export const getSortComparator = (
-  choices,
-  options,
-  valueKey,
-  explicitComparator,
-) => {
+  choices: unknown[] | undefined,
+  options: unknown[] | undefined,
+  valueKey: string | undefined,
+  explicitComparator: SortComparator,
+): SortComparator => {
   if (explicitComparator) {
     return explicitComparator;
   }
@@ -126,14 +186,16 @@ export const getSortComparator = (
   return undefined;
 };
 
-export const innerGetOptions = props => {
-  const { choices, optionRenderer, valueKey } = props;
-  let options = [];
+export const innerGetOptions = (props: SelectControlProps): SelectOption[] => {
+  const { choices, optionRenderer, valueKey = 'value' } = props;
+  let options: SelectOption[] = [];
   if (props.options) {
     options = props.options.map(o => ({
       ...o,
-      value: o[valueKey],
-      label: optionRenderer ? optionRenderer(o) : o.label || o[valueKey],
+      value: o[valueKey] as string | number,
+      label: optionRenderer
+        ? (optionRenderer(o) as string)
+        : ((o.label || o[valueKey]) as string),
     }));
   } else if (choices) {
     // Accepts different formats of input
@@ -142,24 +204,25 @@ export const innerGetOptions = props => {
         const [value, label] = c.length > 1 ? c : [c[0], c[0]];
         return {
           value,
-          label,
+          label: String(label),
         };
       }
-      if (Object.is(c)) {
-        return {
-          ...c,
-          value: c[valueKey],
-          label: c.label || c[valueKey],
-        };
-      }
-      return { value: c, label: c };
+      // This branch handles object-like choices, but choices are typed as tuples
+      return { value: c as unknown as string | number, label: String(c) };
     });
   }
   return options;
 };
 
-export default class SelectControl extends PureComponent {
-  constructor(props) {
+export default class SelectControl extends PureComponent<
+  SelectControlProps,
+  SelectControlState
+> {
+  static propTypes = propTypes;
+
+  static defaultProps = defaultProps;
+
+  constructor(props: SelectControlProps) {
     super(props);
     this.state = {
       options: this.getOptions(props),
@@ -168,7 +231,7 @@ export default class SelectControl extends PureComponent {
     this.handleFilterOptions = this.handleFilterOptions.bind(this);
   }
 
-  componentDidUpdate(prevProps) {
+  componentDidUpdate(prevProps: SelectControlProps) {
     if (
       !isEqualArray(this.props.choices, prevProps.choices) ||
       !isEqualArray(this.props.options, prevProps.options)
@@ -180,30 +243,39 @@ export default class SelectControl extends PureComponent {
 
   // Beware: This is acting like an on-click instead of an on-change
   // (firing every time user chooses vs firing only if a new option is chosen).
-  onChange(val) {
+  onChange(val: SelectValue | SelectOption | SelectOption[]) {
     // will eventually call `exploreReducer`: SET_FIELD_VALUE
-    const { valueKey } = this.props;
-    let onChangeVal = val;
+    const { valueKey = 'value' } = this.props;
+    let onChangeVal: SelectValue = val as SelectValue;
 
     if (Array.isArray(val)) {
       const values = val.map(v =>
-        v?.[valueKey] !== undefined ? v[valueKey] : v,
+        typeof v === 'object' &&
+        v !== null &&
+        (v as SelectOption)[valueKey] !== undefined
+          ? (v as SelectOption)[valueKey]
+          : v,
       );
-      onChangeVal = values;
+      onChangeVal = values as (string | number)[];
     }
-    if (typeof val === 'object' && val?.[valueKey] !== undefined) {
-      onChangeVal = val[valueKey];
+    if (
+      typeof val === 'object' &&
+      val !== null &&
+      !Array.isArray(val) &&
+      (val as SelectOption)[valueKey] !== undefined
+    ) {
+      onChangeVal = (val as SelectOption)[valueKey] as string | number;
     }
-    this.props.onChange(onChangeVal, []);
+    this.props.onChange?.(onChangeVal, []);
   }
 
-  getOptions(props) {
+  getOptions(props: SelectControlProps) {
     return innerGetOptions(props);
   }
 
-  handleFilterOptions(text, option) {
+  handleFilterOptions(text: string, option: SelectOption) {
     const { filterOption } = this.props;
-    return filterOption({ data: option }, text);
+    return filterOption?.({ data: option }, text) ?? true;
   }
 
   render() {
@@ -316,11 +388,9 @@ export default class SelectControl extends PureComponent {
           }
         `}
       >
-        <Select {...selectProps} />
+        {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+        <Select {...(selectProps as any)} />
       </div>
     );
   }
 }
-
-SelectControl.propTypes = propTypes;
-SelectControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/SpatialControl.jsx b/superset-frontend/src/explore/components/controls/SpatialControl.tsx
similarity index 72%
rename from superset-frontend/src/explore/components/controls/SpatialControl.jsx
rename to superset-frontend/src/explore/components/controls/SpatialControl.tsx
index 52575341cb..3ec41e37ff 100644
--- a/superset-frontend/src/explore/components/controls/SpatialControl.jsx
+++ b/superset-frontend/src/explore/components/controls/SpatialControl.tsx
@@ -16,8 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Component } from 'react';
-import PropTypes from 'prop-types';
+import { Component, type ReactNode } from 'react';
 import {
   Row,
   Col,
@@ -35,27 +34,54 @@ const spatialTypes = {
   latlong: 'latlong',
   delimited: 'delimited',
   geohash: 'geohash',
-};
-
-const propTypes = {
-  onChange: PropTypes.func,
-  value: PropTypes.object,
-  animation: PropTypes.bool,
-  choices: PropTypes.array,
-};
-
-const defaultProps = {
-  onChange: () => {},
-  animation: true,
-  choices: [],
-};
-
-export default class SpatialControl extends Component {
-  constructor(props) {
+} as const;
+
+type SpatialType = (typeof spatialTypes)[keyof typeof spatialTypes];
+
+interface SpatialValue {
+  type: SpatialType;
+  latCol?: string;
+  lonCol?: string;
+  lonlatCol?: string;
+  delimiter?: string;
+  reverseCheckbox?: boolean;
+  geohashCol?: string;
+}
+
+interface SpatialControlProps {
+  onChange?: (value: SpatialValue, errors: string[]) => void;
+  value?: SpatialValue;
+  animation?: boolean;
+  choices?: [string, string][];
+}
+
+interface SpatialControlState {
+  type: SpatialType;
+  delimiter: string;
+  latCol: string | undefined;
+  lonCol: string | undefined;
+  lonlatCol: string | undefined;
+  reverseCheckbox: boolean;
+  geohashCol: string | undefined;
+  value: SpatialValue | null;
+  errors: string[];
+}
+
+export default class SpatialControl extends Component<
+  SpatialControlProps,
+  SpatialControlState
+> {
+  static defaultProps = {
+    onChange: () => {},
+    animation: true,
+    choices: [],
+  };
+
+  constructor(props: SpatialControlProps) {
     super(props);
-    const v = props.value || {};
-    let defaultCol;
-    if (props.choices.length > 0) {
+    const v = props.value || ({} as SpatialValue);
+    let defaultCol: string | undefined;
+    if (props.choices && props.choices.length > 0) {
       defaultCol = props.choices[0][0];
     }
     this.state = {
@@ -69,19 +95,16 @@ export default class SpatialControl extends Component {
       value: null,
       errors: [],
     };
-    this.toggleCheckbox = this.toggleCheckbox.bind(this);
-    this.onChange = this.onChange.bind(this);
-    this.renderReverseCheckbox = this.renderReverseCheckbox.bind(this);
   }
 
-  componentDidMount() {
+  componentDidMount(): void {
     this.onChange();
   }
 
-  onChange() {
+  onChange = (): void => {
     const { type } = this.state;
-    const value = { type };
-    const errors = [];
+    const value: SpatialValue = { type };
+    const errors: string[] = [];
     const errMsg = t('Invalid lat/long configuration.');
     if (type === spatialTypes.latlong) {
       value.latCol = this.state.latCol;
@@ -104,21 +127,21 @@ export default class SpatialControl extends Component {
       }
     }
     this.setState({ value, errors });
-    this.props.onChange(value, errors);
-  }
+    this.props.onChange?.(value, errors);
+  };
 
-  setType(type) {
+  setType = (type: SpatialType): void => {
     this.setState({ type }, this.onChange);
-  }
+  };
 
-  toggleCheckbox() {
+  toggleCheckbox = (): void => {
     this.setState(
       prevState => ({ reverseCheckbox: !prevState.reverseCheckbox }),
       this.onChange,
     );
-  }
+  };
 
-  renderLabelContent() {
+  renderLabelContent(): string | null {
     if (this.state.errors.length > 0) {
       return 'N/A';
     }
@@ -134,25 +157,28 @@ export default class SpatialControl extends Component {
     return null;
   }
 
-  renderSelect(name, type) {
+  renderSelect(name: keyof SpatialControlState, type: SpatialType): ReactNode {
     return (
       <SelectControl
         ariaLabel={name}
         name={name}
         choices={this.props.choices}
-        value={this.state[name]}
+        value={this.state[name] as string}
         clearable={false}
         onFocus={() => {
           this.setType(type);
         }}
-        onChange={value => {
-          this.setState({ [name]: value }, this.onChange);
+        onChange={(value: string) => {
+          this.setState(
+            { [name]: value } as unknown as SpatialControlState,
+            this.onChange,
+          );
         }}
       />
     );
   }
 
-  renderReverseCheckbox() {
+  renderReverseCheckbox(): ReactNode {
     return (
       <span>
         {t('Reverse lat/long ')}
@@ -164,13 +190,13 @@ export default class SpatialControl extends Component {
     );
   }
 
-  renderPopoverContent() {
+  renderPopoverContent(): ReactNode {
     return (
       <div style={{ width: '300px' }}>
         <PopoverSection
           title={t('Longitude & Latitude columns')}
           isSelected={this.state.type === spatialTypes.latlong}
-          onSelect={this.setType.bind(this, spatialTypes.latlong)}
+          onSelect={() => this.setType(spatialTypes.latlong)}
         >
           <Row gutter={16}>
             <Col xs={24} md={12}>
@@ -190,7 +216,7 @@ export default class SpatialControl extends Component {
               'Python library for more details',
           )}
           isSelected={this.state.type === spatialTypes.delimited}
-          onSelect={this.setType.bind(this, spatialTypes.delimited)}
+          onSelect={() => this.setType(spatialTypes.delimited)}
         >
           <Row gutter={16}>
             <Col xs={24} md={12}>
@@ -205,7 +231,7 @@ export default class SpatialControl extends Component {
         <PopoverSection
           title={t('Geohash')}
           isSelected={this.state.type === spatialTypes.geohash}
-          onSelect={this.setType.bind(this, spatialTypes.geohash)}
+          onSelect={() => this.setType(spatialTypes.geohash)}
         >
           <Row gutter={16}>
             <Col xs={24} md={12}>
@@ -221,13 +247,13 @@ export default class SpatialControl extends Component {
     );
   }
 
-  render() {
+  render(): ReactNode {
     return (
       <div>
         <ControlHeader {...this.props} />
         <Popover
           content={this.renderPopoverContent()}
-          placement="topLeft" // so that popover doesn't move when label changes
+          placement="topLeft"
           trigger="click"
         >
           <Label className="pointer">{this.renderLabelContent()}</Label>
@@ -236,6 +262,3 @@ export default class SpatialControl extends Component {
     );
   }
 }
-
-SpatialControl.propTypes = propTypes;
-SpatialControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.test.tsx
similarity index 100%
rename from superset-frontend/src/explore/components/controls/TextAreaControl.test.jsx
rename to superset-frontend/src/explore/components/controls/TextAreaControl.test.tsx
diff --git a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx b/superset-frontend/src/explore/components/controls/TextAreaControl.tsx
similarity index 69%
rename from superset-frontend/src/explore/components/controls/TextAreaControl.jsx
rename to superset-frontend/src/explore/components/controls/TextAreaControl.tsx
index 076fa1174e..90b2c352c3 100644
--- a/superset-frontend/src/explore/components/controls/TextAreaControl.jsx
+++ b/superset-frontend/src/explore/components/controls/TextAreaControl.tsx
@@ -33,6 +33,54 @@ import 'ace-builds/src-min-noconflict/mode-handlebars';
 
 import ControlHeader from 'src/explore/components/ControlHeader';
 
+interface HotkeyConfig {
+  name: string;
+  key: string;
+  func: () => void;
+}
+
+interface ThemeType {
+  colorBorder: string;
+  colorBgMask: string;
+  sizeUnit: number;
+}
+
+interface TextAreaControlProps {
+  name?: string;
+  onChange?: (value: string) => void;
+  initialValue?: string;
+  height?: number;
+  minLines?: number;
+  maxLines?: number;
+  offerEditInModal?: boolean;
+  language?:
+    | 'json'
+    | 'html'
+    | 'sql'
+    | 'markdown'
+    | 'javascript'
+    | 'handlebars'
+    | null;
+  aboveEditorSection?: React.ReactNode;
+  readOnly?: boolean;
+  resize?:
+    | 'block'
+    | 'both'
+    | 'horizontal'
+    | 'inline'
+    | 'none'
+    | 'vertical'
+    | null;
+  textAreaStyles?: React.CSSProperties;
+  tooltipOptions?: Record<string, unknown>;
+  hotkeys?: HotkeyConfig[];
+  debounceDelay?: number | null;
+  theme?: ThemeType;
+  'aria-required'?: boolean;
+  value?: string;
+  [key: string]: unknown;
+}
+
 const propTypes = {
   name: PropTypes.string,
   onChange: PropTypes.func,
@@ -82,18 +130,27 @@ const defaultProps = {
   debounceDelay: null,
 };
 
-class TextAreaControl extends Component {
-  constructor(props) {
+class TextAreaControl extends Component<TextAreaControlProps> {
+  static propTypes = propTypes;
+
+  static defaultProps = defaultProps;
+
+  debouncedOnChange:
+    | ReturnType<typeof debounce<(value: string) => void>>
+    | undefined;
+
+  constructor(props: TextAreaControlProps) {
     super(props);
-    if (props.debounceDelay) {
+    if (props.debounceDelay && props.onChange) {
       this.debouncedOnChange = debounce(props.onChange, props.debounceDelay);
     }
   }
 
-  componentDidUpdate(prevProps) {
+  componentDidUpdate(prevProps: TextAreaControlProps) {
     if (
       this.props.onChange !== prevProps.onChange &&
-      this.props.debounceDelay
+      this.props.debounceDelay &&
+      this.props.onChange
     ) {
       if (this.debouncedOnChange) {
         this.debouncedOnChange.cancel();
@@ -105,12 +162,12 @@ class TextAreaControl extends Component {
     }
   }
 
-  handleChange(value) {
+  handleChange(value: string | { target: { value: string } }) {
     const finalValue = typeof value === 'object' ? value.target.value : value;
     if (this.debouncedOnChange) {
       this.debouncedOnChange(finalValue);
     } else {
-      this.props.onChange(finalValue);
+      this.props.onChange?.(finalValue);
     }
   }
 
@@ -123,8 +180,10 @@ class TextAreaControl extends Component {
   renderEditor(inModal = false) {
     const minLines = inModal ? 40 : this.props.minLines || 12;
     if (this.props.language) {
-      const style = {
-        border: `1px solid ${this.props.theme.colorBorder}`,
+      const style: React.CSSProperties = {
+        border: this.props.theme?.colorBorder
+          ? `1px solid ${this.props.theme.colorBorder}`
+          : undefined,
         minHeight: `${minLines}em`,
         width: 'auto',
         ...this.props.textAreaStyles,
@@ -133,10 +192,18 @@ class TextAreaControl extends Component {
         style.resize = this.props.resize;
       }
       if (this.props.readOnly) {
-        style.backgroundColor = this.props.theme.colorBgMask;
+        style.backgroundColor = this.props.theme?.colorBgMask;
       }
-      const onEditorLoad = editor => {
-        this.props.hotkeys.forEach(keyConfig => {
+      const onEditorLoad = (editor: {
+        commands: {
+          addCommand: (cmd: {
+            name: string;
+            bindKey: { win: string; mac: string };
+            exec: () => void;
+          }) => void;
+        };
+      }) => {
+        this.props.hotkeys?.forEach(keyConfig => {
           editor.commands.addCommand({
             name: keyConfig.name,
             bindKey: { win: keyConfig.key, mac: keyConfig.key },
@@ -203,16 +270,17 @@ class TextAreaControl extends Component {
         {this.renderEditor()}
         {this.props.offerEditInModal && (
           <ModalTrigger
-            modalTitle={controlHeader}
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
+            modalTitle={controlHeader as any}
             triggerNode={
               <Button
                 buttonSize="small"
-                style={{ marginTop: this.props.theme.sizeUnit }}
+                style={{ marginTop: this.props.theme?.sizeUnit ?? 4 }}
               >
                 {t('Edit %s in modal', this.props.language)}
               </Button>
             }
-            modalBody={this.renderModalBody(true)}
+            modalBody={this.renderModalBody()}
             responsive
           />
         )}
@@ -221,7 +289,5 @@ class TextAreaControl extends Component {
   }
 }
 
-TextAreaControl.propTypes = propTypes;
-TextAreaControl.defaultProps = defaultProps;
-
-export default withTheme(TextAreaControl);
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export default withTheme(TextAreaControl as any);
diff --git a/superset-frontend/src/explore/components/controls/TextControl/index.tsx b/superset-frontend/src/explore/components/controls/TextControl/index.tsx
index d55d47860a..805d3d90eb 100644
--- a/superset-frontend/src/explore/components/controls/TextControl/index.tsx
+++ b/superset-frontend/src/explore/components/controls/TextControl/index.tsx
@@ -25,7 +25,9 @@ import { Constants, Input } from '@superset-ui/core/components';
 type InputValueType = string | number;
 
 export interface TextControlProps<T extends InputValueType = InputValueType> {
+  name?: string;
   label?: string;
+  description?: string;
   disabled?: boolean;
   isFloat?: boolean;
   isInt?: boolean;
@@ -36,6 +38,8 @@ export interface TextControlProps<T extends InputValueType = InputValueType> {
   controlId?: string;
   renderTrigger?: boolean;
   validationErrors?: string[];
+  hovered?: boolean;
+  showHeader?: boolean;
 }
 
 export interface TextControlState {
diff --git a/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx b/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx
similarity index 81%
rename from superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx
rename to superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx
index c10922b0e2..d614c4d563 100644
--- a/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx
+++ b/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx
@@ -34,6 +34,42 @@ import BoundsControl from '../BoundsControl';
 import CheckboxControl from '../CheckboxControl';
 import ControlPopover from '../ControlPopover/ControlPopover';
 
+interface TimeSeriesColumnControlProps {
+  label?: string;
+  tooltip?: string;
+  colType?: string;
+  width?: string;
+  height?: string;
+  timeLag?: string | number;
+  timeRatio?: string;
+  comparisonType?: string;
+  showYAxis?: boolean;
+  yAxisBounds?: (number | null)[];
+  bounds?: (number | null)[];
+  d3format?: string;
+  dateFormat?: string;
+  sparkType?: string;
+  onChange?: (state: TimeSeriesColumnControlState) => void;
+}
+
+interface TimeSeriesColumnControlState {
+  label: string;
+  tooltip: string;
+  colType: string;
+  width: string;
+  height: string;
+  timeLag: string | number;
+  timeRatio: string;
+  comparisonType: string;
+  showYAxis: boolean;
+  yAxisBounds: (number | null)[];
+  bounds: (number | null)[];
+  d3format: string;
+  dateFormat: string;
+  sparkType: string;
+  popoverVisible: boolean;
+}
+
 const propTypes = {
   label: PropTypes.string,
   tooltip: PropTypes.string,
@@ -111,8 +147,15 @@ const ButtonBar = styled.div`
   justify-content: center;
 `;
 
-export default class TimeSeriesColumnControl extends Component {
-  constructor(props) {
+export default class TimeSeriesColumnControl extends Component<
+  TimeSeriesColumnControlProps,
+  TimeSeriesColumnControlState
+> {
+  static propTypes = propTypes;
+
+  static defaultProps = defaultProps;
+
+  constructor(props: TimeSeriesColumnControlProps) {
     super(props);
 
     this.onSave = this.onSave.bind(this);
@@ -124,22 +167,22 @@ export default class TimeSeriesColumnControl extends Component {
     this.state = this.initialState();
   }
 
-  initialState() {
+  initialState(): TimeSeriesColumnControlState {
     return {
-      label: this.props.label,
-      tooltip: this.props.tooltip,
-      colType: this.props.colType,
-      width: this.props.width,
-      height: this.props.height,
-      timeLag: this.props.timeLag || 0,
-      timeRatio: this.props.timeRatio,
-      comparisonType: this.props.comparisonType,
-      showYAxis: this.props.showYAxis,
-      yAxisBounds: this.props.yAxisBounds,
-      bounds: this.props.bounds,
-      d3format: this.props.d3format,
-      dateFormat: this.props.dateFormat,
-      sparkType: this.props.sparkType,
+      label: this.props.label ?? t('Time series columns'),
+      tooltip: this.props.tooltip ?? '',
+      colType: this.props.colType ?? '',
+      width: this.props.width ?? '',
+      height: this.props.height ?? '',
+      timeLag: this.props.timeLag ?? 0,
+      timeRatio: this.props.timeRatio ?? '',
+      comparisonType: this.props.comparisonType ?? '',
+      showYAxis: this.props.showYAxis ?? false,
+      yAxisBounds: this.props.yAxisBounds ?? [null, null],
+      bounds: this.props.bounds ?? [null, null],
+      d3format: this.props.d3format ?? '',
+      dateFormat: this.props.dateFormat ?? '',
+      sparkType: this.props.sparkType ?? 'line',
       popoverVisible: false,
     };
   }
@@ -150,7 +193,7 @@ export default class TimeSeriesColumnControl extends Component {
   }
 
   onSave() {
-    this.props.onChange(this.state);
+    this.props.onChange?.(this.state);
     this.setState({ popoverVisible: false });
   }
 
@@ -158,23 +201,23 @@ export default class TimeSeriesColumnControl extends Component {
     this.resetState();
   }
 
-  onSelectChange(attr, opt) {
-    this.setState({ [attr]: opt });
+  onSelectChange(attr: string, opt: string) {
+    this.setState(prevState => ({ ...prevState, [attr]: opt }));
   }
 
-  onTextInputChange(attr, event) {
-    this.setState({ [attr]: event.target.value });
+  onTextInputChange(attr: string, event: React.ChangeEvent<HTMLInputElement>) {
+    this.setState(prevState => ({ ...prevState, [attr]: event.target.value }));
   }
 
-  onCheckboxChange(attr, value) {
-    this.setState({ [attr]: value });
+  onCheckboxChange(attr: string, value: boolean) {
+    this.setState(prevState => ({ ...prevState, [attr]: value }));
   }
 
-  onBoundsChange(bounds) {
+  onBoundsChange(bounds: (number | null)[]) {
     this.setState({ bounds });
   }
 
-  onPopoverVisibleChange(popoverVisible) {
+  onPopoverVisibleChange(popoverVisible: boolean) {
     if (popoverVisible) {
       this.setState({ popoverVisible });
     } else {
@@ -182,15 +225,20 @@ export default class TimeSeriesColumnControl extends Component {
     }
   }
 
-  onYAxisBoundsChange(yAxisBounds) {
+  onYAxisBoundsChange(yAxisBounds: (number | null)[]) {
     this.setState({ yAxisBounds });
   }
 
   textSummary() {
-    return `${this.props.label}`;
+    return `${this.props.label ?? ''}`;
   }
 
-  formRow(label, tooltip, ttLabel, control) {
+  formRow(
+    label: string,
+    tooltip: string,
+    ttLabel: string,
+    control: React.ReactNode,
+  ) {
     return (
       <StyledRow>
         <StyledCol xs={24} md={11}>
@@ -412,6 +460,3 @@ export default class TimeSeriesColumnControl extends Component {
     );
   }
 }
-
-TimeSeriesColumnControl.propTypes = propTypes;
-TimeSeriesColumnControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.test.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.test.tsx
similarity index 100%
rename from superset-frontend/src/explore/components/controls/ViewportControl.test.jsx
rename to superset-frontend/src/explore/components/controls/ViewportControl.test.tsx
diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.tsx
similarity index 62%
rename from superset-frontend/src/explore/components/controls/ViewportControl.jsx
rename to superset-frontend/src/explore/components/controls/ViewportControl.tsx
index daf82d14f0..431a2505e2 100644
--- a/superset-frontend/src/explore/components/controls/ViewportControl.jsx
+++ b/superset-frontend/src/explore/components/controls/ViewportControl.tsx
@@ -16,16 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Component } from 'react';
+import { Component, type ReactNode } from 'react';
 import { t } from '@superset-ui/core';
-import PropTypes from 'prop-types';
 import { Popover, FormLabel, Label } from '@superset-ui/core/components';
 import { decimal2sexagesimal } from 'geolib';
 
 import TextControl from './TextControl';
 import ControlHeader from '../ControlHeader';
 
-export const DEFAULT_VIEWPORT = {
+export interface Viewport {
+  longitude: number;
+  latitude: number;
+  zoom: number;
+  bearing: number;
+  pitch: number;
+}
+
+export const DEFAULT_VIEWPORT: Viewport = {
   longitude: 6.85236157047845,
   latitude: 31.222656842808707,
   zoom: 1,
@@ -33,54 +40,49 @@ export const DEFAULT_VIEWPORT = {
   pitch: 0,
 };
 
-const PARAMS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch'];
+const PARAMS: (keyof Viewport)[] = [
+  'longitude',
+  'latitude',
+  'zoom',
+  'bearing',
+  'pitch',
+];
 
-const propTypes = {
-  onChange: PropTypes.func,
-  value: PropTypes.shape({
-    longitude: PropTypes.number,
-    latitude: PropTypes.number,
-    zoom: PropTypes.number,
-    bearing: PropTypes.number,
-    pitch: PropTypes.number,
-  }),
-  default: PropTypes.object,
-  name: PropTypes.string.isRequired,
-};
-
-const defaultProps = {
-  onChange: () => {},
-  default: { type: 'fix', value: 5 },
-  value: DEFAULT_VIEWPORT,
-};
+interface ViewportControlProps {
+  onChange?: (value: Viewport) => void;
+  value?: Viewport;
+  default?: Record<string, unknown>;
+  name: string;
+}
 
-export default class ViewportControl extends Component {
-  constructor(props) {
-    super(props);
-    this.onChange = this.onChange.bind(this);
-  }
+export default class ViewportControl extends Component<ViewportControlProps> {
+  static defaultProps = {
+    onChange: () => {},
+    default: { type: 'fix', value: 5 },
+    value: DEFAULT_VIEWPORT,
+  };
 
-  onChange(ctrl, value) {
-    this.props.onChange({
-      ...this.props.value,
+  onChange = (ctrl: keyof Viewport, value: number): void => {
+    this.props.onChange?.({
+      ...this.props.value!,
       [ctrl]: value,
     });
-  }
+  };
 
-  renderTextControl(ctrl) {
+  renderTextControl(ctrl: keyof Viewport): ReactNode {
     return (
       <div key={ctrl}>
         <FormLabel>{ctrl}</FormLabel>
         <TextControl
-          value={this.props.value[ctrl]}
-          onChange={this.onChange.bind(this, ctrl)}
+          value={this.props.value?.[ctrl]}
+          onChange={(value: number) => this.onChange(ctrl, value)}
           isFloat
         />
       </div>
     );
   }
 
-  renderPopover() {
+  renderPopover(): ReactNode {
     return (
       <div id={`filter-popover-${this.props.name}`}>
         {PARAMS.map(ctrl => this.renderTextControl(ctrl))}
@@ -88,8 +90,8 @@ export default class ViewportControl extends Component {
     );
   }
 
-  renderLabel() {
-    if (this.props.value.longitude && this.props.value.latitude) {
+  renderLabel(): string {
+    if (this.props.value?.longitude && this.props.value?.latitude) {
       return `${decimal2sexagesimal(
         this.props.value.longitude,
       )} | ${decimal2sexagesimal(this.props.value.latitude)}`;
@@ -97,12 +99,11 @@ export default class ViewportControl extends Component {
     return 'N/A';
   }
 
-  render() {
+  render(): ReactNode {
     return (
       <div>
         <ControlHeader {...this.props} />
         <Popover
-          container={document.body}
           trigger="click"
           placement="right"
           content={this.renderPopover()}
@@ -114,6 +115,3 @@ export default class ViewportControl extends Component {
     );
   }
 }
-
-ViewportControl.propTypes = propTypes;
-ViewportControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/components/controls/XAxisSortControl.tsx b/superset-frontend/src/explore/components/controls/XAxisSortControl.tsx
index a6ea631527..373e88f605 100644
--- a/superset-frontend/src/explore/components/controls/XAxisSortControl.tsx
+++ b/superset-frontend/src/explore/components/controls/XAxisSortControl.tsx
@@ -19,11 +19,15 @@
 import { useEffect, useState } from 'react';
 import SelectControl from './SelectControl';
 
-export default function XAxisSortControl(props: {
+interface XAxisSortControlProps {
   onChange: (val: string | undefined) => void;
   value: string | null;
   shouldReset: boolean;
-}) {
+  name?: string;
+  [key: string]: unknown;
+}
+
+export default function XAxisSortControl(props: XAxisSortControlProps) {
   const [value, setValue] = useState(props.value);
   useEffect(() => {
     if (props.shouldReset) {
@@ -32,5 +36,11 @@ export default function XAxisSortControl(props: {
     }
   }, [props.shouldReset, props.value]);
 
-  return <SelectControl {...props} value={value} />;
+  return (
+    <SelectControl
+      {...props}
+      name={props.name ?? 'x_axis_sort'}
+      value={value}
+    />
+  );
 }
diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
index 6bdf4a263b..0462b9e3a1 100644
--- a/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
+++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.ts
@@ -25,7 +25,6 @@ import {
   QueryFormMetric,
 } from '@superset-ui/core';
 import {
-  ControlStateMapping,
   getStandardizedControls,
   isStandardizedFormData,
   StandardizedControls,
@@ -189,11 +188,10 @@ export class StandardizedFormData {
 
   transform(
     targetVizType: string,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     exploreState: Record<string, any>,
-  ): {
-    formData: QueryFormData;
-    controlsState: ControlStateMapping;
-  } {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  ): any {
     /*
      * Transform form_data between different viz. Return new form_data and controlsState.
      * 1. get memorized form_data by viz type or get previous form_data
@@ -211,13 +209,15 @@ export class StandardizedFormData {
         publicFormData[key] = exploreState.form_data[key];
       }
     });
-    const targetControlsState = getControlsState(exploreState, {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const targetControlsState = getControlsState(exploreState as any, {
       ...latestFormData,
       ...publicFormData,
       viz_type: targetVizType,
     });
     const targetFormData = {
-      ...getFormDataFromControls(targetControlsState),
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      ...getFormDataFromControls(targetControlsState as any),
       // Preserve dashboard context when switching viz types.
       ...(publicFormData.dashboardId && {
         dashboardId: publicFormData.dashboardId,
@@ -243,14 +243,18 @@ export class StandardizedFormData {
       getStandardizedControls().clear();
       rv = {
         formData: transformed,
-        controlsState: getControlsState(exploreState, transformed),
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        controlsState: getControlsState(exploreState as any, transformed),
       };
     }
 
     // refresh validator message
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     rv.controlsState = getControlsState(
-      { ...exploreState, controls: rv.controlsState },
-      rv.formData,
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      { ...exploreState, controls: rv.controlsState } as any,
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      rv.formData as any,
     );
     return rv;
   }
diff --git a/superset-frontend/src/explore/controls.jsx b/superset-frontend/src/explore/controls.tsx
similarity index 87%
rename from superset-frontend/src/explore/controls.jsx
rename to superset-frontend/src/explore/controls.tsx
index 6deea0274a..3decceb081 100644
--- a/superset-frontend/src/explore/controls.jsx
+++ b/superset-frontend/src/explore/controls.tsx
@@ -56,6 +56,7 @@
  * in tandem with `controlPanels/index.js` that defines how controls are composed into sections for
  * each and every visualization type.
  */
+import type { Column, SequentialScheme } from '@superset-ui/core';
 import {
   t,
   getCategoricalSchemeRegistry,
@@ -67,6 +68,24 @@ import { formatSelectOptions } from 'src/explore/exploreUtils';
 import { TIME_FILTER_LABELS } from './constants';
 import { StyledColumnOption } from './components/optionRenderers';
 
+interface Datasource {
+  columns: Column[];
+  metrics: unknown[];
+  granularity_sqla?: Column[];
+  main_dttm_col?: string;
+  time_grain_sqla?: unknown[];
+}
+
+interface ControlState {
+  datasource?: Datasource;
+  controls?: Record<string, { value?: unknown }>;
+}
+
+interface ControlConfig {
+  includeTime?: boolean;
+  [key: string]: unknown;
+}
+
 const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
 const sequentialSchemeRegistry = getSequentialSchemeRegistry();
 
@@ -126,18 +145,20 @@ const groupByControl = {
     'One or many columns to group by. High cardinality groupings should include a series limit ' +
       'to limit the number of fetched and rendered series.',
   ),
-  optionRenderer: c => <StyledColumnOption column={c} showType />,
+  optionRenderer: (c: Column) => <StyledColumnOption column={c} showType />,
   valueKey: 'column_name',
-  filterOption: ({ data: opt }, text) =>
+  filterOption: ({ data: opt }: { data: Column }, text: string) =>
     (opt.column_name &&
       opt.column_name.toLowerCase().indexOf(text.toLowerCase()) >= 0) ||
     (opt.verbose_name &&
       opt.verbose_name.toLowerCase().indexOf(text.toLowerCase()) >= 0),
-  mapStateToProps: (state, control) => {
-    const newState = {};
+  mapStateToProps: (state: ControlState, control: ControlConfig) => {
+    const newState: { options?: Column[] } = {};
     if (state.datasource) {
-      newState.options = state.datasource.columns.filter(c => c.groupby);
-      if (control && control.includeTime) {
+      newState.options = state.datasource.columns.filter(
+        (c: Column) => c.groupby,
+      );
+      if (control?.includeTime) {
         newState.options.push(timeColumnOption);
       }
     }
@@ -150,7 +171,7 @@ const metrics = {
   multi: true,
   label: t('Metrics'),
   validators: [validateNonEmpty],
-  mapStateToProps: state => {
+  mapStateToProps: (state: ControlState) => {
     const { datasource } = state;
     return {
       columns: datasource ? datasource.columns : [],
@@ -167,10 +188,18 @@ const metric = {
   description: t('Metric'),
 };
 
-export function columnChoices(datasource) {
-  if (datasource && datasource.columns) {
+export function columnChoices(
+  datasource: Datasource | null | undefined,
+): [string, string][] {
+  if (datasource?.columns) {
     return datasource.columns
-      .map(col => [col.column_name, col.verbose_name || col.column_name])
+      .map(
+        col =>
+          [col.column_name, col.verbose_name || col.column_name] as [
+            string,
+            string,
+          ],
+      )
       .sort((opt1, opt2) =>
         opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1,
       );
@@ -188,7 +217,7 @@ export const controls = {
     label: t('Dataset'),
     default: null,
     description: null,
-    mapStateToProps: ({ datasource }) => ({
+    mapStateToProps: ({ datasource }: ControlState) => ({
       datasource,
       isEditable: !!datasource,
     }),
@@ -219,7 +248,13 @@ export const controls = {
     type: 'ColorSchemeControl',
     label: t('Linear color scheme'),
     choices: () =>
-      sequentialSchemeRegistry.values().map(value => [value.id, value.label]),
+      sequentialSchemeRegistry
+        .values()
+        .filter(
+          (value): value is SequentialScheme =>
+            value !== undefined && 'id' in value && 'label' in value,
+        )
+        .map(value => [value.id, value.label]),
     default: sequentialSchemeRegistry.getDefaultKey(),
     clearable: false,
     description: '',
@@ -285,10 +320,10 @@ export const controls = {
         'expression',
     ),
     clearable: false,
-    optionRenderer: c => <StyledColumnOption column={c} showType />,
+    optionRenderer: (c: Column) => <StyledColumnOption column={c} showType />,
     valueKey: 'column_name',
-    mapStateToProps: state => {
-      const props = {};
+    mapStateToProps: (state: ControlState) => {
+      const props: { choices?: Column[]; default?: string | null } = {};
       if (state.datasource) {
         props.choices = state.datasource.granularity_sqla;
         props.default = null;
@@ -313,7 +348,7 @@ export const controls = {
         'The options here are defined on a per database ' +
         'engine basis in the Superset source code.',
     ),
-    mapStateToProps: state => ({
+    mapStateToProps: (state: ControlState) => ({
       choices: state.datasource ? state.datasource.time_grain_sqla : null,
     }),
   },
@@ -367,7 +402,7 @@ export const controls = {
       'Metric used to define how the top series are sorted if a series or row limit is present. ' +
         'If undefined reverts to the first metric (where appropriate).',
     ),
-    mapStateToProps: state => ({
+    mapStateToProps: (state: ControlState) => ({
       columns: state.datasource ? state.datasource.columns : [],
       savedMetrics: state.datasource ? state.datasource.metrics : [],
       datasource: state.datasource,
@@ -423,11 +458,9 @@ export const controls = {
     default: 'SMART_NUMBER',
     choices: D3_FORMAT_OPTIONS,
     description: D3_FORMAT_DOCS,
-    mapStateToProps: state => {
+    mapStateToProps: (state: ControlState) => {
       const showWarning =
-        state.controls &&
-        state.controls.comparison_type &&
-        state.controls.comparison_type.value === 'percentage';
+        state.controls?.comparison_type?.value === 'percentage';
       return {
         warning: showWarning
           ? t(
@@ -445,9 +478,9 @@ export const controls = {
     label: t('Filters'),
     default: null,
     description: '',
-    mapStateToProps: state => ({
+    mapStateToProps: (state: ControlState) => ({
       columns: state.datasource
-        ? state.datasource.columns.filter(c => c.filterable)
+        ? state.datasource.columns.filter((c: Column) => c.filterable)
         : [],
       savedMetrics: state.datasource ? state.datasource.metrics : [],
       datasource: state.datasource,
diff --git a/superset-frontend/src/explore/store.test.jsx b/superset-frontend/src/explore/store.test.tsx
similarity index 95%
rename from superset-frontend/src/explore/store.test.jsx
rename to superset-frontend/src/explore/store.test.tsx
index aad4d1f8e4..602390a4d4 100644
--- a/superset-frontend/src/explore/store.test.jsx
+++ b/superset-frontend/src/explore/store.test.tsx
@@ -39,9 +39,8 @@ describe('store', () => {
 
   // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
   describe('applyDefaultFormData', () => {
-    window.featureFlags = {
-      SCOPED_FILTER: false,
-    };
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    (window as any).featureFlags = {};
 
     test('applies default to formData if the key is missing', () => {
       const inputFormData = {
diff --git a/superset-frontend/src/explore/store.js b/superset-frontend/src/explore/store.ts
similarity index 61%
rename from superset-frontend/src/explore/store.js
rename to superset-frontend/src/explore/store.ts
index 9c2fa52127..b1189efff4 100644
--- a/superset-frontend/src/explore/store.js
+++ b/superset-frontend/src/explore/store.ts
@@ -17,11 +17,33 @@
  * under the License.
  */
 /* eslint camelcase: 0 */
-import { getChartControlPanelRegistry, VizType } from '@superset-ui/core';
+import {
+  DatasourceType,
+  getChartControlPanelRegistry,
+  VizType,
+} from '@superset-ui/core';
+import type { QueryFormData } from '@superset-ui/core';
 import { getAllControlsState, getFormDataFromControls } from './controlUtils';
 import { controls } from './controls';
 
-function handleDeprecatedControls(formData) {
+interface ExploreState {
+  common?: {
+    conf: {
+      DEFAULT_VIZ_TYPE?: string;
+    };
+  };
+  datasource: {
+    type: string;
+  };
+}
+
+type FormData = QueryFormData & {
+  y_axis_zero?: boolean;
+  y_axis_bounds?: [number | null, number | null];
+  datasource?: string;
+};
+
+function handleDeprecatedControls(formData: FormData): void {
   // Reaffectation / handling of deprecated controls
   /* eslint-disable no-param-reassign */
 
@@ -31,7 +53,10 @@ function handleDeprecatedControls(formData) {
   }
 }
 
-export function getControlsState(state, inputFormData) {
+export function getControlsState(
+  state: ExploreState,
+  inputFormData: FormData,
+): Record<string, unknown> {
   /*
    * Gets a new controls object to put in the state. The controls object
    * is similar to the configuration control with only the controls
@@ -45,10 +70,11 @@ export function getControlsState(state, inputFormData) {
     formData.viz_type || state.common?.conf.DEFAULT_VIZ_TYPE || VizType.Table;
 
   handleDeprecatedControls(formData);
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   const controlsState = getAllControlsState(
     vizType,
-    state.datasource.type,
-    state,
+    state.datasource.type as DatasourceType,
+    state as any,
     formData,
   );
 
@@ -60,39 +86,45 @@ export function getControlsState(state, inputFormData) {
   return controlsState;
 }
 
-export function applyDefaultFormData(inputFormData) {
-  const datasourceType = inputFormData.datasource.split('__')[1];
+export function applyDefaultFormData(
+  inputFormData: FormData,
+): Record<string, unknown> {
+  const datasourceType = inputFormData.datasource?.split('__')[1] ?? '';
   const vizType = inputFormData.viz_type;
   const controlsState = getAllControlsState(
     vizType,
-    datasourceType,
+    datasourceType as DatasourceType,
     null,
     inputFormData,
   );
-  const controlFormData = getFormDataFromControls(controlsState);
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const controlFormData = getFormDataFromControls(controlsState as any);
 
-  const formData = {};
+  const formData: Record<string, unknown> = {};
   Object.keys(controlsState)
     .concat(Object.keys(inputFormData))
     .forEach(controlName => {
-      if (inputFormData[controlName] === undefined) {
+      if (inputFormData[controlName as keyof FormData] === undefined) {
         formData[controlName] = controlFormData[controlName];
       } else {
-        formData[controlName] = inputFormData[controlName];
+        formData[controlName] = inputFormData[controlName as keyof FormData];
       }
     });
 
   return formData;
 }
 
-const defaultControls = { ...controls };
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const defaultControls: Record<string, any> = { ...controls };
 Object.keys(controls).forEach(f => {
-  defaultControls[f].value = controls[f].default;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  defaultControls[f].value = (controls as any)[f].default;
 });
 
 const defaultState = {
   controls: defaultControls,
-  form_data: getFormDataFromControls(defaultControls),
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  form_data: getFormDataFromControls(defaultControls as any),
 };
 
 export { defaultControls, defaultState };