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

rusackas pushed a commit to branch chore/ts-migration-sqllab-explore
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 4458fa4dcd6ec1a6da1047fb60eccee3031e2ba9
Author: Evan Rusackas <[email protected]>
AuthorDate: Thu Dec 18 23:19:18 2025 -0800

    chore(frontend): migrate SqlLab and explore JS/JSX files to TypeScript
    
    Migrate 40 files from JavaScript/JSX to TypeScript:
    
    SqlLab:
    - actions/sqlLab.ts - Redux actions with typed thunks
    - middlewares/persistSqlLabStateEnhancer.ts - localStorage persistence
    - reducers/sqlLab.ts - Reducer with SqlLabState types
    
    Explore:
    - store.ts - Store initialization with ExploreState types
    - controls.tsx - Control definitions with Datasource interface
    
    Controls (33 files):
    - CheckboxControl, ViewportControl, SpatialControl with full interfaces
    - AnnotationLayer, AnnotationTypes with proper type exports
    - AdhocFilter, AdhocMetric, MetricsControl conversions
    - SelectControl, TextAreaControl, DatasourceControl
    - CollectionControl, FixedOrMetricControl, TimeSeriesColumnControl
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude Opus 4.5 <[email protected]>
---
 .../src/SqlLab/actions/{sqlLab.js => sqlLab.ts}    | 207 ++++++++++++++++-----
 .../middlewares/persistSqlLabStateEnhancer.js      | 136 --------------
 .../middlewares/persistSqlLabStateEnhancer.ts      | 184 ++++++++++++++++++
 .../src/SqlLab/reducers/{sqlLab.js => sqlLab.ts}   |  22 ++-
 .../{AnnotationLayer.jsx => AnnotationLayer.tsx}   |   0
 .../{AnnotationTypes.js => AnnotationTypes.ts}     |  33 +++-
 ...undsControl.test.jsx => BoundsControl.test.tsx} |   0
 .../{CheckboxControl.jsx => CheckboxControl.tsx}   |  46 ++---
 .../CollectionControl/{index.jsx => index.tsx}     |   0
 .../DatasourceControl/{index.jsx => index.tsx}     |   0
 ...erTitle.jsx => DndColumnSelectPopoverTitle.tsx} |   0
 .../{AdhocFilter.test.js => AdhocFilter.test.ts}   |   0
 .../AdhocFilter/{index.js => index.ts}             |   0
 .../AdhocFilterControl/{index.jsx => index.tsx}    |   0
 ...er.test.jsx => AdhocFilterEditPopover.test.tsx} |   0
 .../{index.jsx => index.tsx}                       |   0
 .../FixedOrMetricControl/{index.jsx => index.tsx}  |   0
 .../{AdhocMetric.test.js => AdhocMetric.test.ts}   |   0
 .../{AdhocMetric.js => AdhocMetric.ts}             |   0
 .../{index.jsx => index.tsx}                       |   0
 ...cOption.test.jsx => AdhocMetricOption.test.tsx} |   0
 ...AdhocMetricOption.jsx => AdhocMetricOption.tsx} |   0
 ...on.test.jsx => FilterDefinitionOption.test.tsx} |   0
 ...nitionOption.jsx => FilterDefinitionOption.tsx} |   0
 ...lue.test.jsx => MetricDefinitionValue.test.tsx} |   0
 ...finitionValue.jsx => MetricDefinitionValue.tsx} |   0
 ...icsControl.test.jsx => MetricsControl.test.tsx} |   0
 .../{MetricsControl.jsx => MetricsControl.tsx}     |   0
 .../{adhocMetricType.js => adhocMetricType.ts}     |   0
 ...lectControl.test.jsx => SelectControl.test.tsx} |   0
 .../{SelectControl.jsx => SelectControl.tsx}       |   0
 .../{SpatialControl.jsx => SpatialControl.tsx}     | 123 +++++++-----
 ...eaControl.test.jsx => TextAreaControl.test.tsx} |   0
 .../{TextAreaControl.jsx => TextAreaControl.tsx}   |   0
 .../{index.jsx => index.tsx}                       |   0
 ...rtControl.test.jsx => ViewportControl.test.tsx} |   0
 .../{ViewportControl.jsx => ViewportControl.tsx}   |  81 ++++----
 .../src/explore/{controls.jsx => controls.tsx}     |  29 ++-
 .../src/explore/{store.test.jsx => store.test.tsx} |   0
 .../src/explore/{store.js => store.ts}             |  37 +++-
 40 files changed, 572 insertions(+), 326 deletions(-)

diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js 
b/superset-frontend/src/SqlLab/actions/sqlLab.ts
similarity index 89%
rename from superset-frontend/src/SqlLab/actions/sqlLab.js
rename to superset-frontend/src/SqlLab/actions/sqlLab.ts
index fd23cb5753..eb8991aec8 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, ThunkDispatch } from 'redux-thunk';
+import type { QueryResponse, SupersetError } from '@superset-ui/core';
 import {
   FeatureFlag,
   SupersetClient,
@@ -38,10 +40,63 @@ 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;
+  catalog?: string | null;
+  schema?: string | null;
+  tempTable?: string;
+  templateParams?: string;
+  queryLimit?: number;
+  runAsync?: boolean;
+  ctas?: boolean;
+  ctas_method?: string;
+  isDataPreview?: boolean;
+  progress?: number;
+  startDttm?: number;
+  state?: string;
+  cached?: boolean;
+  resultsKey?: string;
+  updateTabState?: boolean;
+  tableName?: string;
+  link?: string;
+  inLocalStorage?: boolean;
+}
+
+export interface Database {
+  id: number;
+  allow_run_async: boolean;
+  disable_data_preview?: boolean;
+}
+
+interface SqlLabAction {
+  type: string;
+  [key: string]: unknown;
+}
+
+type SqlLabThunkAction<R = void> = ThunkAction<
+  R,
+  SqlLabRootState,
+  unknown,
+  SqlLabAction
+>;
+
+type SqlLabThunkDispatch = ThunkDispatch<
+  SqlLabRootState,
+  unknown,
+  SqlLabAction
+>;
+
 export const RESET_STATE = 'RESET_STATE';
 export const ADD_QUERY_EDITOR = 'ADD_QUERY_EDITOR';
 export const UPDATE_QUERY_EDITOR = 'UPDATE_QUERY_EDITOR';
@@ -112,13 +167,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,13 +183,19 @@ 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) {
+export function getUpToDateQuery(
+  rootState: SqlLabRootState,
+  queryEditor: QueryEditor | { id: string },
+  key?: string,
+): QueryEditor {
   const {
     sqlLab: { unsavedQueryEditor, queryEditors },
   } = rootState;
@@ -142,10 +204,12 @@ export function getUpToDateQuery(rootState, queryEditor, 
key) {
     id,
     ...queryEditors.find(qe => qe.id === id),
     ...(id === unsavedQueryEditor.id && unsavedQueryEditor),
-  };
+  } as QueryEditor;
 }
 
-export function resetState(data) {
+export function resetState(
+  data?: Record<string, unknown>,
+): SqlLabThunkAction<void> {
   return (dispatch, getState) => {
     const { common } = getState();
     const initialState = getInitialState({
@@ -162,15 +226,19 @@ export function resetState(data) {
   };
 }
 
-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) {
+export function scheduleQuery(
+  query: Record<string, unknown>,
+): SqlLabThunkAction<Promise<void>> {
   return dispatch =>
     SupersetClient.post({
       endpoint: '/api/v1/saved_query/',
@@ -191,7 +259,9 @@ export function scheduleQuery(query) {
       );
 }
 
-export function estimateQueryCost(queryEditor) {
+export function estimateQueryCost(
+  queryEditor: QueryEditor,
+): SqlLabThunkAction<Promise<unknown[]>> {
   return (dispatch, getState) => {
     const { dbId, catalog, schema, sql, selectedText, templateParams } =
       getUpToDateQuery(getState(), queryEditor);
@@ -232,11 +302,14 @@ 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,
+): SqlLabAction {
   Object.assign(query, {
     id: query.id ? query.id : nanoid(11),
     progress: 0,
@@ -247,11 +320,17 @@ export function startQuery(query, runPreviewOnly) {
   return { type: START_QUERY, query, runPreviewOnly };
 }
 
-export function querySuccess(query, results) {
+export function querySuccess(
+  query: Query,
+  results: QueryResponse,
+): SqlLabAction {
   return { type: QUERY_SUCCESS, query, results };
 }
 
-export function logFailedQuery(query, errors) {
+export function logFailedQuery(
+  query: Query,
+  errors?: SupersetError[],
+): SqlLabThunkAction<void> {
   return function (dispatch) {
     const eventData = {
       has_err: true,
@@ -259,7 +338,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,34 +353,48 @@ export function logFailedQuery(query, errors) {
   };
 }
 
-export function createQueryFailedAction(query, msg, link, errors) {
+export function createQueryFailedAction(
+  query: Query,
+  msg: string,
+  link?: string,
+  errors?: SupersetError[],
+): SqlLabAction {
   return { type: QUERY_FAILED, query, msg, link, errors };
 }
 
-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) {
+export function stopQuery(query: Query): SqlLabAction {
   return { type: STOP_QUERY, query };
 }
 
-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) {
+export function fetchQueryResults(
+  query: Query,
+  displayLimit?: number,
+  timeoutInMs?: number,
+): SqlLabThunkAction<Promise<void>> {
   return function (dispatch, getState) {
     const { SQLLAB_QUERY_RESULT_TIMEOUT } = getState().common?.conf ?? {};
     dispatch(requestQueryResults(query));
@@ -315,7 +410,7 @@ 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 QueryResponse)))
       .catch(response => {
         controller.abort();
         getClientErrorObject(response).then(error => {
@@ -332,7 +427,10 @@ export function fetchQueryResults(query, displayLimit, 
timeoutInMs) {
   };
 }
 
-export function runQuery(query, runPreviewOnly) {
+export function runQuery(
+  query: Query,
+  runPreviewOnly?: boolean,
+): SqlLabThunkAction<Promise<void>> {
   return function (dispatch) {
     dispatch(startQuery(query, runPreviewOnly));
     const postPayload = {
@@ -361,7 +459,7 @@ export function runQuery(query, runPreviewOnly) {
     })
       .then(({ json }) => {
         if (!query.runAsync) {
-          dispatch(querySuccess(query, json));
+          dispatch(querySuccess(query, json as QueryResponse));
         }
       })
       .catch(response =>
@@ -381,16 +479,17 @@ export function runQuery(query, runPreviewOnly) {
 }
 
 export function runQueryFromSqlEditor(
-  database,
-  queryEditor,
-  defaultQueryLimit,
-  tempTable,
-  ctas,
-  ctasMethod,
-) {
+  database: Database | null,
+  queryEditor: QueryEditor,
+  defaultQueryLimit: number,
+  tempTable?: string,
+  ctas?: boolean,
+  ctasMethod?: string,
+): SqlLabThunkAction<void> {
   return function (dispatch, getState) {
     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,14 +509,14 @@ export function runQueryFromSqlEditor(
   };
 }
 
-export function reRunQuery(query) {
+export function reRunQuery(query: Query): SqlLabThunkAction<void> {
   // run Query with a new id
   return function (dispatch) {
     dispatch(runQuery({ ...query, id: nanoid(11) }));
   };
 }
 
-export function postStopQuery(query) {
+export function postStopQuery(query: Query): SqlLabThunkAction<Promise<void>> {
   return function (dispatch) {
     return SupersetClient.post({
       endpoint: '/api/v1/query/stop',
@@ -430,11 +529,17 @@ export function postStopQuery(query) {
   };
 }
 
-export function setDatabases(databases) {
+export function setDatabases(
+  databases: Record<string, Database>,
+): SqlLabAction {
   return { type: SET_DATABASES, databases };
 }
 
-function migrateTable(table, queryEditorId, dispatch) {
+function migrateTable(
+  table: Table,
+  queryEditorId: string,
+  dispatch: SqlLabThunkDispatch,
+): Promise<void> {
   return SupersetClient.post({
     endpoint: encodeURI('/tableschemaview/'),
     postPayload: { table: { ...table, queryEditorId } },
@@ -459,7 +564,11 @@ function migrateTable(table, queryEditorId, dispatch) {
     );
 }
 
-function migrateQuery(queryId, queryEditorId, dispatch) {
+function migrateQuery(
+  queryId: string,
+  queryEditorId: string,
+  dispatch: SqlLabThunkDispatch,
+): Promise<void> {
   return SupersetClient.post({
     endpoint: encodeURI(`/tabstateview/${queryEditorId}/migrate_query`),
     postPayload: { queryId },
@@ -486,7 +595,9 @@ function migrateQuery(queryId, queryEditorId, dispatch) {
  * stored in local storage will also be synchronized to the backend
  * through syncQueryEditor.
  */
-export function syncQueryEditor(queryEditor) {
+export function syncQueryEditor(
+  queryEditor: QueryEditor,
+): SqlLabThunkAction<Promise<void[] | void>> {
   return function (dispatch, getState) {
     const { tables, queries } = getState().sqlLab;
     const localStorageTables = tables.filter(
@@ -513,10 +624,10 @@ export function syncQueryEditor(queryEditor) {
         });
         return Promise.all([
           ...localStorageTables.map(table =>
-            migrateTable(table, newQueryEditor.tabViewId, dispatch),
+            migrateTable(table, newQueryEditor.tabViewId!, dispatch),
           ),
           ...localStorageQueries.map(query =>
-            migrateQuery(query.id, newQueryEditor.tabViewId, dispatch),
+            migrateQuery(query.id, newQueryEditor.tabViewId!, dispatch),
           ),
         ]);
       })
@@ -533,7 +644,9 @@ export function syncQueryEditor(queryEditor) {
   };
 }
 
-export function addQueryEditor(queryEditor) {
+export function addQueryEditor(
+  queryEditor: Partial<QueryEditor>,
+): SqlLabAction {
   const newQueryEditor = {
     ...queryEditor,
     id: nanoid(11),
@@ -707,7 +820,7 @@ export function fetchQueryEditor(queryEditor, displayLimit) 
{
         };
         dispatch(loadQueryEditor(loadedQueryEditor));
         dispatch(setTables(json.table_schemas || []));
-        if (json.latest_query && json.latest_query.resultsKey) {
+        if (json.latest_query?.resultsKey) {
           dispatch(fetchQueryResults(json.latest_query, displayLimit));
         }
       })
@@ -812,7 +925,8 @@ export function queryEditorSetTitle(queryEditor, name, id) {
 }
 
 export function saveQuery(query, clientId) {
-  const { id, ...payload } = convertQueryToServer(query);
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const { id: _id, ...payload } = convertQueryToServer(query);
 
   return dispatch =>
     SupersetClient.post({
@@ -858,7 +972,8 @@ export const addSavedQueryToTabState =
   };
 
 export function updateSavedQuery(query, clientId) {
-  const { id, ...payload } = convertQueryToServer(query);
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  const { id: _id, ...payload } = convertQueryToServer(query);
 
   return dispatch =>
     SupersetClient.put({
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..cd0aee25ff
--- /dev/null
+++ b/superset-frontend/src/SqlLab/middlewares/persistSqlLabStateEnhancer.ts
@@ -0,0 +1,184 @@
+/**
+ * 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
+export const persistSqlLabStateEnhancer = persistState(
+  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 97%
rename from superset-frontend/src/SqlLab/reducers/sqlLab.js
rename to superset-frontend/src/SqlLab/reducers/sqlLab.ts
index 3469036da4..8a533c4140 100644
--- a/superset-frontend/src/SqlLab/reducers/sqlLab.js
+++ b/superset-frontend/src/SqlLab/reducers/sqlLab.ts
@@ -20,6 +20,7 @@ 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 } from '../types';
 import * as actions from '../actions/sqlLab';
 import {
   addToObject,
@@ -31,7 +32,19 @@ import {
   extendArr,
 } from '../../reduxUtils';
 
-function alterUnsavedQueryEditorState(state, updatedState, id, silent = false) 
{
+type SqlLabState = SqlLabRootState['sqlLab'];
+
+interface SqlLabAction {
+  type: string;
+  [key: string]: unknown;
+}
+
+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 +65,11 @@ 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 {
+  const actionHandlers: Record<string, () => SqlLabState> = {
     [actions.ADD_QUERY_EDITOR]() {
       const mergeUnsavedState = alterInArr(
         state,
diff --git 
a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx
 
b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx
rename to 
superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js
 
b/superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts
similarity index 75%
rename from 
superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.js
rename to 
superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationTypes.ts
index cdf7a161f7..d8534d8f55 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,9 +86,9 @@ 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 };
   }
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 70%
rename from 
superset-frontend/src/explore/components/controls/CheckboxControl.jsx
rename to superset-frontend/src/explore/components/controls/CheckboxControl.tsx
index dd551dd996..b3e5b3cb66 100644
--- a/superset-frontend/src/explore/components/controls/CheckboxControl.jsx
+++ b/superset-frontend/src/explore/components/controls/CheckboxControl.tsx
@@ -16,22 +16,16 @@
  * 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?: string;
+  onChange?: (value: boolean) => void;
+}
 
 const CheckBoxControlWrapper = styled.div`
   ${({ theme }) => css`
@@ -47,28 +41,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 +70,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 100%
rename from 
superset-frontend/src/explore/components/controls/CollectionControl/index.jsx
rename to 
superset-frontend/src/explore/components/controls/CollectionControl/index.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx 
b/superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx
rename to 
superset-frontend/src/explore/components/controls/DatasourceControl/index.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx
 
b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx
rename to 
superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.js
rename to 
superset-frontend/src/explore/components/controls/FilterControl/AdhocFilter/index.ts
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 100%
rename from 
superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.jsx
rename to 
superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterControl/index.tsx
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 100%
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
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 100%
rename from 
superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.jsx
rename to 
superset-frontend/src/explore/components/controls/FilterControl/AdhocFilterEditPopover/index.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx
 
b/superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.jsx
rename to 
superset-frontend/src/explore/components/controls/FixedOrMetricControl/index.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.js
rename to 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.test.ts
diff --git 
a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js
 
b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.js
rename to 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetric.ts
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 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopover/index.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.test.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
 
b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricOption.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.test.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx
 
b/superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/FilterDefinitionOption.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.test.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
 
b/superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/MetricDefinitionValue.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.test.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
 
b/superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.jsx
rename to 
superset-frontend/src/explore/components/controls/MetricControl/MetricsControl.tsx
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/SelectControl.test.jsx 
b/superset-frontend/src/explore/components/controls/SelectControl.test.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/SelectControl.test.jsx
rename to 
superset-frontend/src/explore/components/controls/SelectControl.test.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/SelectControl.jsx 
b/superset-frontend/src/explore/components/controls/SelectControl.tsx
similarity index 100%
rename from superset-frontend/src/explore/components/controls/SelectControl.jsx
rename to superset-frontend/src/explore/components/controls/SelectControl.tsx
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 100%
rename from 
superset-frontend/src/explore/components/controls/TextAreaControl.jsx
rename to superset-frontend/src/explore/components/controls/TextAreaControl.tsx
diff --git 
a/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx
 
b/superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx
similarity index 100%
rename from 
superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.jsx
rename to 
superset-frontend/src/explore/components/controls/TimeSeriesColumnControl/index.tsx
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 63%
rename from 
superset-frontend/src/explore/components/controls/ViewportControl.jsx
rename to superset-frontend/src/explore/components/controls/ViewportControl.tsx
index daf82d14f0..91ca0f1727 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,7 +99,7 @@ export default class ViewportControl extends Component {
     return 'N/A';
   }
 
-  render() {
+  render(): ReactNode {
     return (
       <div>
         <ControlHeader {...this.props} />
@@ -114,6 +116,3 @@ export default class ViewportControl extends Component {
     );
   }
 }
-
-ViewportControl.propTypes = propTypes;
-ViewportControl.defaultProps = defaultProps;
diff --git a/superset-frontend/src/explore/controls.jsx 
b/superset-frontend/src/explore/controls.tsx
similarity index 96%
rename from superset-frontend/src/explore/controls.jsx
rename to superset-frontend/src/explore/controls.tsx
index 6deea0274a..0c85b83301 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 } from '@superset-ui/core';
 import {
   t,
   getCategoricalSchemeRegistry,
@@ -67,6 +68,14 @@ 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[];
+}
+
 const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
 const sequentialSchemeRegistry = getSequentialSchemeRegistry();
 
@@ -137,7 +146,7 @@ const groupByControl = {
     const newState = {};
     if (state.datasource) {
       newState.options = state.datasource.columns.filter(c => c.groupby);
-      if (control && control.includeTime) {
+      if (control?.includeTime) {
         newState.options.push(timeColumnOption);
       }
     }
@@ -167,10 +176,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,
       );
@@ -425,9 +442,7 @@ export const controls = {
     description: D3_FORMAT_DOCS,
     mapStateToProps: state => {
       const showWarning =
-        state.controls &&
-        state.controls.comparison_type &&
-        state.controls.comparison_type.value === 'percentage';
+        state.controls?.comparison_type?.value === 'percentage';
       return {
         warning: showWarning
           ? t(
diff --git a/superset-frontend/src/explore/store.test.jsx 
b/superset-frontend/src/explore/store.test.tsx
similarity index 100%
rename from superset-frontend/src/explore/store.test.jsx
rename to superset-frontend/src/explore/store.test.tsx
diff --git a/superset-frontend/src/explore/store.js 
b/superset-frontend/src/explore/store.ts
similarity index 77%
rename from superset-frontend/src/explore/store.js
rename to superset-frontend/src/explore/store.ts
index 9c2fa52127..d7fbf359c5 100644
--- a/superset-frontend/src/explore/store.js
+++ b/superset-frontend/src/explore/store.ts
@@ -18,10 +18,28 @@
  */
 /* eslint camelcase: 0 */
 import { 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 +49,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
@@ -60,8 +81,10 @@ 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,
@@ -71,14 +94,14 @@ export function applyDefaultFormData(inputFormData) {
   );
   const controlFormData = getFormDataFromControls(controlsState);
 
-  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];
       }
     });
 

Reply via email to