diff --git a/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js b/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js
index 8baf5616d0784140ab068acfba8bf3f4abf34476..2731910b9f54c1a070594418474c304c434aa81a 100644
--- a/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js
+++ b/e2e/test/scenarios/dashboard-cards/click-behavior.cy.spec.js
@@ -944,6 +944,8 @@ describeEE("scenarios > dashboard > dashboard cards > click behavior", () => {
         },
       ]);
 
+      cy.go("back");
+      cy.log("return to the dashboard");
       cy.go("back");
       testChangingBackToDefaultBehavior();
     });
diff --git a/frontend/src/metabase-lib/v1/Question.ts b/frontend/src/metabase-lib/v1/Question.ts
index a71d30eec3a9152cb1c3fab5dfac96640c369adc..11ef44a4c22a627221ec0f83809e19cdf69c2ff3 100644
--- a/frontend/src/metabase-lib/v1/Question.ts
+++ b/frontend/src/metabase-lib/v1/Question.ts
@@ -50,7 +50,7 @@ import type {
   LastEditInfo,
   ParameterId,
   Parameter as ParameterObject,
-  ParameterValues,
+  ParameterValuesMap,
   TableId,
   UserInfo,
   VisualizationSettings,
@@ -64,7 +64,7 @@ export type QuestionCreatorOpts = {
   tableId?: TableId;
   collectionId?: CollectionId;
   metadata?: Metadata;
-  parameterValues?: ParameterValues;
+  parameterValues?: ParameterValuesMap;
   type?: "query" | "native";
   name?: string;
   display?: CardDisplayType;
@@ -93,7 +93,7 @@ class Question {
    * Parameter values mean either the current values of dashboard filters or SQL editor template parameters.
    * They are in the grey area between UI state and question state, but having them in Question wrapper is convenient.
    */
-  _parameterValues: ParameterValues;
+  _parameterValues: ParameterValuesMap;
 
   private __mlv2Query: Lib.Query | undefined;
 
@@ -105,7 +105,7 @@ class Question {
   constructor(
     card: any,
     metadata?: Metadata,
-    parameterValues?: ParameterValues,
+    parameterValues?: ParameterValuesMap,
   ) {
     this._card = card;
     this._metadata =
diff --git a/frontend/src/metabase-types/store/qb.ts b/frontend/src/metabase-types/store/qb.ts
index ad910bcbfa0ac1930c6be6e4d63b7f0d87e12e7f..580f5bbdcd9b9abcb72286abcb74cbae10d2ff60 100644
--- a/frontend/src/metabase-types/store/qb.ts
+++ b/frontend/src/metabase-types/store/qb.ts
@@ -1,3 +1,4 @@
+import type { Deferred } from "metabase/lib/promise";
 import type { QueryModalType } from "metabase/query_builder/constants";
 import type { Widget } from "metabase/visualizations/components/ChartSettings/types";
 import type {
@@ -69,7 +70,7 @@ export interface QueryBuilderState {
   queryStatus: QueryBuilderQueryStatus;
   queryResults: Dataset[] | null;
   queryStartTime: number | null;
-  cancelQueryDeferred: Promise<void> | null;
+  cancelQueryDeferred: Deferred<void> | null;
 
   card: Card | null;
   originalCard: Card | null;
diff --git a/frontend/src/metabase/nav/components/search/SearchBar/SearchBar.tsx b/frontend/src/metabase/nav/components/search/SearchBar/SearchBar.tsx
index 4515754a22a3345ebea13226b4c61b6219fa2c79..9e0ebb0aa73c181175bdac3131d90ef4a654a1ee 100644
--- a/frontend/src/metabase/nav/components/search/SearchBar/SearchBar.tsx
+++ b/frontend/src/metabase/nav/components/search/SearchBar/SearchBar.tsx
@@ -92,7 +92,7 @@ function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
       // if we're already looking at the right model, don't navigate, just update the zoomed in row
       const isSameModel = result?.model_id === location?.state?.cardId;
       if (isSameModel && result.model === "indexed-entity") {
-        zoomInRow({ objectId: result.id })(dispatch);
+        dispatch(zoomInRow({ objectId: result.id }));
       } else {
         onChangeLocation(result.getUrl());
       }
diff --git a/frontend/src/metabase/plugins/index.ts b/frontend/src/metabase/plugins/index.ts
index 504102161dbbb19030834d860b5fc36a366166cf..82522b52f26cd0f1613e012217a0670fad054a91 100644
--- a/frontend/src/metabase/plugins/index.ts
+++ b/frontend/src/metabase/plugins/index.ts
@@ -225,7 +225,7 @@ const defaultLoginPageIllustration = {
   isDefault: true,
 };
 
-const getLoadingMessage = (isSlow: boolean) =>
+const getLoadingMessage = (isSlow: boolean = false) =>
   isSlow ? t`Waiting for results...` : t`Doing science...`;
 
 // selectors that customize behavior between app versions
diff --git a/frontend/src/metabase/query_builder/actions/core/core.js b/frontend/src/metabase/query_builder/actions/core/core.ts
similarity index 88%
rename from frontend/src/metabase/query_builder/actions/core/core.js
rename to frontend/src/metabase/query_builder/actions/core/core.ts
index 946b1b53bf3a2ae2fc63a3fe2c191c5a3f456b42..fa54bc6f5c76eae399eaad25f28a2cf905de0e1d 100644
--- a/frontend/src/metabase/query_builder/actions/core/core.js
+++ b/frontend/src/metabase/query_builder/actions/core/core.ts
@@ -8,6 +8,7 @@ import Questions from "metabase/entities/questions";
 import Revision from "metabase/entities/revisions";
 import { shouldOpenInBlankWindow } from "metabase/lib/dom";
 import { createThunkAction } from "metabase/lib/redux";
+import { isNotNull } from "metabase/lib/types";
 import * as Urls from "metabase/lib/urls";
 import { copy } from "metabase/lib/utils";
 import { loadMetadataForCard } from "metabase/questions/actions";
@@ -22,6 +23,14 @@ import {
   cardIsEquivalent,
   cardQueryIsEquivalent,
 } from "metabase-lib/v1/queries/utils/card";
+import type {
+  Card,
+  Database,
+  DatasetQuery,
+  ParameterId,
+  ParameterValuesMap,
+} from "metabase-types/api";
+import type { Dispatch, GetState } from "metabase-types/store";
 
 import { trackNewQuestionSaved } from "../../analytics";
 import {
@@ -65,21 +74,27 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
 
     dispatch(resetQB());
 
+    if (!outdatedQuestion) {
+      return;
+    }
+
     const action = await dispatch(
       Questions.actions.fetch({ id: outdatedQuestion.id() }, { reload: true }),
     );
     const card = Questions.HACK_getObjectFromAction(action);
 
-    // We need to manually massage the paramters into the parameterValues shape,
+    // We need to manually massage the parameters into the parameterValues shape,
     // to be able to pass them to new Question.
     // We could use _parameterValues here but prefer not to use internal fields.
-    const parameterValues = outdatedQuestion.parameters().reduce(
-      (acc, next) => ({
-        ...acc,
-        [next.id]: next.value,
-      }),
-      {},
-    );
+    const parameterValues: ParameterValuesMap = outdatedQuestion
+      .parameters()
+      .reduce(
+        (acc, next) => ({
+          ...acc,
+          [next.id]: next.value,
+        }),
+        {},
+      );
 
     const question = new Question(
       card,
@@ -110,8 +125,11 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
  *     - `navigateToNewCardInsideQB` is being called (see below)
  */
 export const SET_CARD_AND_RUN = "metabase/qb/SET_CARD_AND_RUN";
-export const setCardAndRun = (nextCard, { shouldUpdateUrl = true } = {}) => {
-  return async (dispatch, getState) => {
+export const setCardAndRun = (
+  nextCard: Card,
+  { shouldUpdateUrl = true } = {},
+) => {
+  return async (dispatch: Dispatch, getState: GetState) => {
     // clone
     const card = copy(nextCard);
 
@@ -183,25 +201,30 @@ export const navigateToNewCardInsideQB = createThunkAction(
 
 // DEPRECATED, still used in a couple places
 export const setDatasetQuery =
-  (datasetQuery, options) => (dispatch, getState) => {
+  (datasetQuery: DatasetQuery) => (dispatch: Dispatch, getState: GetState) => {
     if (datasetQuery instanceof Query) {
       datasetQuery = datasetQuery.datasetQuery();
     }
 
     const question = getQuestion(getState());
-    dispatch(updateQuestion(question.setDatasetQuery(datasetQuery), options));
+
+    if (!question) {
+      return;
+    }
+
+    dispatch(updateQuestion(question.setDatasetQuery(datasetQuery)));
   };
 
 export const API_CREATE_QUESTION = "metabase/qb/API_CREATE_QUESTION";
-export const apiCreateQuestion = question => {
-  return async (dispatch, getState) => {
+export const apiCreateQuestion = (question: Question) => {
+  return async (dispatch: Dispatch, getState: GetState) => {
     const submittableQuestion = getSubmittableQuestion(getState(), question);
     const createdQuestion = await reduxCreateQuestion(
       submittableQuestion,
       dispatch,
     );
 
-    const databases = Databases.selectors.getList(getState());
+    const databases: Database[] = Databases.selectors.getList(getState());
     if (databases && !databases.some(d => d.is_saved_questions)) {
       dispatch({ type: Databases.actionTypes.INVALIDATE_LISTS_ACTION });
     }
@@ -224,12 +247,17 @@ export const apiCreateQuestion = question => {
     if (isModel || isMetric) {
       dispatch(runQuestionQuery());
     }
+
+    return createdQuestion;
   };
 };
 
 export { API_UPDATE_QUESTION };
-export const apiUpdateQuestion = (question, { rerunQuery } = {}) => {
-  return async (dispatch, getState) => {
+export const apiUpdateQuestion = (
+  question: Question,
+  { rerunQuery }: { rerunQuery?: boolean } = {},
+) => {
+  return async (dispatch: Dispatch, getState: GetState) => {
     const originalQuestion = getOriginalQuestion(getState());
     question = question || getQuestion(getState());
 
@@ -286,7 +314,7 @@ export const apiUpdateQuestion = (question, { rerunQuery } = {}) => {
 export const SET_PARAMETER_VALUE = "metabase/qb/SET_PARAMETER_VALUE";
 export const setParameterValue = createAction(
   SET_PARAMETER_VALUE,
-  (parameterId, value) => {
+  (parameterId: ParameterId, value: string | string[]) => {
     return { id: parameterId, value: normalizeValue(value) };
   },
 );
@@ -307,7 +335,7 @@ export const setParameterValueToDefault = createThunkAction(
   },
 );
 
-function normalizeValue(value) {
+function normalizeValue(value: string | string[]) {
   if (value === "") {
     return null;
   }
@@ -330,14 +358,14 @@ export const revertToRevision = createThunkAction(
   },
 );
 
-async function reduxCreateQuestion(question, dispatch) {
+async function reduxCreateQuestion(question: Question, dispatch: Dispatch) {
   const action = await dispatch(Questions.actions.create(question.card()));
   return question.setCard(Questions.HACK_getObjectFromAction(action));
 }
 
 async function reduxUpdateQuestion(
-  question,
-  dispatch,
+  question: Question,
+  dispatch: Dispatch,
   { excludeDatasetQuery = false, excludeVisualisationSettings = false },
 ) {
   const fullCard = question.card();
@@ -345,7 +373,7 @@ async function reduxUpdateQuestion(
   const keysToOmit = [
     excludeDatasetQuery ? "dataset_query" : null,
     excludeVisualisationSettings ? "visualization_settings" : null,
-  ].filter(Boolean);
+  ].filter(isNotNull);
 
   const card = _.omit(fullCard, ...keysToOmit);
 
diff --git a/frontend/src/metabase/query_builder/actions/core/types.js b/frontend/src/metabase/query_builder/actions/core/types.ts
similarity index 100%
rename from frontend/src/metabase/query_builder/actions/core/types.js
rename to frontend/src/metabase/query_builder/actions/core/types.ts
diff --git a/frontend/src/metabase/query_builder/actions/models.js b/frontend/src/metabase/query_builder/actions/models.js
deleted file mode 100644
index 4dcf7b72c035032d8484c5a2b5df2f59b344287d..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/actions/models.js
+++ /dev/null
@@ -1,85 +0,0 @@
-import { push } from "react-router-redux";
-import { createAction } from "redux-actions";
-import { t } from "ttag";
-
-import Questions from "metabase/entities/questions";
-import { loadMetadataForCard } from "metabase/questions/actions";
-import { addUndo } from "metabase/redux/undo";
-import { getMetadata } from "metabase/selectors/metadata";
-
-import { getQuestion } from "../selectors";
-
-import { API_UPDATE_QUESTION, apiUpdateQuestion, updateQuestion } from "./core";
-import { runDirtyQuestionQuery, runQuestionQuery } from "./querying";
-import { setQueryBuilderMode } from "./ui";
-
-export const setDatasetEditorTab = datasetEditorTab => dispatch => {
-  dispatch(
-    setQueryBuilderMode("dataset", { datasetEditorTab, replaceState: false }),
-  );
-  dispatch(runDirtyQuestionQuery());
-};
-
-export const onCancelCreateNewModel = () => async dispatch => {
-  await dispatch(push("/"));
-};
-
-export const turnQuestionIntoModel = () => async (dispatch, getState) => {
-  const question = getQuestion(getState());
-
-  await dispatch(
-    Questions.actions.update(
-      {
-        id: question.id(),
-      },
-      question
-        .setType("model")
-        .setPinned(true)
-        .setDisplay("table")
-        .setSettings({})
-        .card(),
-    ),
-  );
-
-  const metadata = getMetadata(getState());
-  const dataset = metadata.question(question.id());
-
-  await dispatch(loadMetadataForCard(dataset.card()));
-
-  await dispatch({ type: API_UPDATE_QUESTION, payload: dataset.card() });
-
-  await dispatch(
-    runQuestionQuery({
-      shouldUpdateUrl: true,
-    }),
-  );
-
-  dispatch(
-    addUndo({
-      message: t`This is a model now.`,
-      actions: [apiUpdateQuestion(question, { rerunQuery: true })],
-    }),
-  );
-};
-
-export const turnModelIntoQuestion = () => async (dispatch, getState) => {
-  const model = getQuestion(getState());
-  const question = model.setType("question");
-  await dispatch(apiUpdateQuestion(question, { rerunQuery: true }));
-
-  dispatch(
-    addUndo({
-      message: t`This is a question now.`,
-      actions: [apiUpdateQuestion(model)],
-    }),
-  );
-};
-
-export const SET_METADATA_DIFF = "metabase/qb/SET_METADATA_DIFF";
-export const setMetadataDiff = createAction(SET_METADATA_DIFF);
-
-export const onModelPersistenceChange = isEnabled => (dispatch, getState) => {
-  const question = getQuestion(getState());
-  const nextQuestion = question.setPersisted(isEnabled);
-  dispatch(updateQuestion(nextQuestion, { shouldStartAdHocQuestion: false }));
-};
diff --git a/frontend/src/metabase/query_builder/actions/models.ts b/frontend/src/metabase/query_builder/actions/models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e02e20855d3abbe5210308a76a95a1405dcd7dc4
--- /dev/null
+++ b/frontend/src/metabase/query_builder/actions/models.ts
@@ -0,0 +1,108 @@
+import { push } from "react-router-redux";
+import { createAction } from "redux-actions";
+import { t } from "ttag";
+
+import Questions from "metabase/entities/questions";
+import { loadMetadataForCard } from "metabase/questions/actions";
+import { addUndo } from "metabase/redux/undo";
+import { getMetadata } from "metabase/selectors/metadata";
+import type { Dispatch, GetState } from "metabase-types/store";
+
+import { getQuestion } from "../selectors";
+
+import { API_UPDATE_QUESTION, apiUpdateQuestion, updateQuestion } from "./core";
+import { runDirtyQuestionQuery, runQuestionQuery } from "./querying";
+import { setQueryBuilderMode } from "./ui";
+
+export const setDatasetEditorTab =
+  (datasetEditorTab: "query" | "metadata") => (dispatch: Dispatch) => {
+    dispatch(
+      setQueryBuilderMode("dataset", { datasetEditorTab, replaceState: false }),
+    );
+    dispatch(runDirtyQuestionQuery());
+  };
+
+export const onCancelCreateNewModel = () => async (dispatch: Dispatch) => {
+  await dispatch(push("/"));
+};
+
+export const turnQuestionIntoModel =
+  () => async (dispatch: Dispatch, getState: GetState) => {
+    const question = getQuestion(getState());
+
+    if (!question) {
+      return;
+    }
+
+    await dispatch(
+      Questions.actions.update(
+        {
+          id: question.id(),
+        },
+        question
+          .setType("model")
+          .setPinned(true)
+          .setDisplay("table")
+          .setSettings({})
+          .card(),
+      ),
+    );
+
+    const metadata = getMetadata(getState());
+    const dataset = metadata.question(question.id());
+
+    if (!dataset) {
+      return;
+    }
+
+    await dispatch(loadMetadataForCard(dataset.card()));
+
+    await dispatch({ type: API_UPDATE_QUESTION, payload: dataset.card() });
+
+    await dispatch(
+      runQuestionQuery({
+        shouldUpdateUrl: true,
+      }),
+    );
+
+    dispatch(
+      addUndo({
+        message: t`This is a model now.`,
+        actions: [apiUpdateQuestion(question, { rerunQuery: true })],
+      }),
+    );
+  };
+
+export const turnModelIntoQuestion =
+  () => async (dispatch: Dispatch, getState: GetState) => {
+    const model = getQuestion(getState());
+
+    if (!model) {
+      return;
+    }
+
+    const question = model.setType("question");
+    await dispatch(apiUpdateQuestion(question, { rerunQuery: true }));
+
+    dispatch(
+      addUndo({
+        message: t`This is a question now.`,
+        actions: [apiUpdateQuestion(model)],
+      }),
+    );
+  };
+
+export const SET_METADATA_DIFF = "metabase/qb/SET_METADATA_DIFF";
+export const setMetadataDiff = createAction(SET_METADATA_DIFF);
+
+export const onModelPersistenceChange =
+  (isEnabled: boolean) => (dispatch: Dispatch, getState: GetState) => {
+    const question = getQuestion(getState());
+
+    if (!question) {
+      return;
+    }
+
+    const nextQuestion = question.setPersisted(isEnabled);
+    dispatch(updateQuestion(nextQuestion, { shouldStartAdHocQuestion: false }));
+  };
diff --git a/frontend/src/metabase/query_builder/actions/navigation.js b/frontend/src/metabase/query_builder/actions/navigation.ts
similarity index 89%
rename from frontend/src/metabase/query_builder/actions/navigation.js
rename to frontend/src/metabase/query_builder/actions/navigation.ts
index 9b4db3013dbaa06a632999887fe65f0c01117d88..4a9ac89daa1845043d3fc7843f5ef1c63e04b9d0 100644
--- a/frontend/src/metabase/query_builder/actions/navigation.js
+++ b/frontend/src/metabase/query_builder/actions/navigation.ts
@@ -1,3 +1,4 @@
+import type { Location, LocationDescriptor } from "history";
 import { push, replace } from "react-router-redux";
 import { createAction } from "redux-actions";
 import { parse as parseUrl } from "url";
@@ -7,7 +8,9 @@ import { createThunkAction } from "metabase/lib/redux";
 import { equals } from "metabase/lib/utils";
 import { getLocation } from "metabase/selectors/routing";
 import * as Lib from "metabase-lib";
+import type Question from "metabase-lib/v1/Question";
 import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
+import type { Dispatch } from "metabase-types/store";
 
 import {
   getCard,
@@ -25,7 +28,7 @@ import {
   getURLForCardState,
 } from "../utils";
 
-import { initializeQB, setCardAndRun } from "./core";
+import { type QueryParams, initializeQB, setCardAndRun } from "./core";
 import { resetRowZoom, zoomInRow } from "./object-detail";
 import { cancelQuery } from "./querying";
 import { resetUIControls, setQueryBuilderMode } from "./ui";
@@ -90,7 +93,7 @@ export const popState = createThunkAction(
   },
 );
 
-const getURL = (location, { includeMode = false } = {}) =>
+const getURL = (location: Location, { includeMode = false } = {}) =>
   // strip off trailing queryBuilderMode
   (includeMode
     ? location.pathname
@@ -100,7 +103,8 @@ const getURL = (location, { includeMode = false } = {}) =>
 
 // Logic for handling location changes, dispatched by top-level QueryBuilder component
 export const locationChanged =
-  (location, nextLocation, nextParams) => dispatch => {
+  (location: Location, nextLocation: Location, nextParams: QueryParams) =>
+  (dispatch: Dispatch) => {
     if (location !== nextLocation) {
       if (nextLocation.action === "POP") {
         if (
@@ -126,7 +130,7 @@ export const UPDATE_URL = "metabase/qb/UPDATE_URL";
 export const updateUrl = createThunkAction(
   UPDATE_URL,
   (
-    question,
+    question?: Question | null,
     {
       dirty,
       replaceState,
@@ -139,6 +143,10 @@ export const updateUrl = createThunkAction(
     (dispatch, getState) => {
       if (!question) {
         question = getQuestion(getState());
+
+        if (!question) {
+          return;
+        }
       }
 
       if (dirty == null) {
@@ -180,14 +188,14 @@ export const updateUrl = createThunkAction(
       const url = getURLForCardState(newState, dirty, queryParams, objectId);
 
       const urlParsed = parseUrl(url);
-      const locationDescriptor = {
+      const locationDescriptor: LocationDescriptor = {
         pathname: getPathNameFromQueryBuilderMode({
           pathname: urlParsed.pathname || "",
           queryBuilderMode,
           datasetEditorTab,
         }),
-        search: urlParsed.search,
-        hash: urlParsed.hash,
+        search: urlParsed.search ?? undefined,
+        hash: urlParsed.hash ?? undefined,
         state: newState,
       };
 
@@ -197,15 +205,17 @@ export const updateUrl = createThunkAction(
         (locationDescriptor.hash || "") === (window.location.hash || "");
       const isSameCard =
         currentState && isEqualCard(currentState.card, newState.card);
-      const isSameMode =
-        getQueryBuilderModeFromLocation(locationDescriptor).mode ===
-        getQueryBuilderModeFromLocation(window.location).mode;
 
       if (isSameCard && isSameURL) {
         return;
       }
 
       if (replaceState == null) {
+        const isSameMode =
+          getQueryBuilderModeFromLocation(locationDescriptor)
+            .queryBuilderMode ===
+          getQueryBuilderModeFromLocation(window.location).queryBuilderMode;
+
         // if the serialized card is identical replace the previous state instead of adding a new one
         // e.x. when saving a new card we want to replace the state and URL with one with the new card ID
         replaceState = isSameCard && isSameMode;
diff --git a/frontend/src/metabase/query_builder/actions/object-detail.js b/frontend/src/metabase/query_builder/actions/object-detail.ts
similarity index 79%
rename from frontend/src/metabase/query_builder/actions/object-detail.js
rename to frontend/src/metabase/query_builder/actions/object-detail.ts
index 8b57ee433b2e8b4afdaee108bfb53e0074b4d8e6..f5164667c78f696d978686d4c9bf1344f4ab7e14 100644
--- a/frontend/src/metabase/query_builder/actions/object-detail.js
+++ b/frontend/src/metabase/query_builder/actions/object-detail.ts
@@ -3,8 +3,13 @@ import _ from "underscore";
 import { createThunkAction } from "metabase/lib/redux";
 import { getMetadata } from "metabase/selectors/metadata";
 import { MetabaseApi } from "metabase/services";
+import type { ObjectId } from "metabase/visualizations/components/ObjectDetail/types";
 import * as Lib from "metabase-lib";
 import Question from "metabase-lib/v1/Question";
+import type Field from "metabase-lib/v1/metadata/Field";
+import type ForeignKey from "metabase-lib/v1/metadata/ForeignKey";
+import type { Card, DatasetColumn, FieldId } from "metabase-types/api";
+import type { Dispatch, GetState } from "metabase-types/store";
 
 import {
   getCanZoomNextRow,
@@ -22,8 +27,8 @@ import { updateUrl } from "./navigation";
 
 export const ZOOM_IN_ROW = "metabase/qb/ZOOM_IN_ROW";
 export const zoomInRow =
-  ({ objectId }) =>
-  (dispatch, getState) => {
+  ({ objectId }: { objectId: ObjectId }) =>
+  (dispatch: Dispatch, getState: GetState) => {
     dispatch({ type: ZOOM_IN_ROW, payload: { objectId } });
 
     // don't show object id in url if it is a row index
@@ -32,12 +37,16 @@ export const zoomInRow =
   };
 
 export const RESET_ROW_ZOOM = "metabase/qb/RESET_ROW_ZOOM";
-export const resetRowZoom = () => dispatch => {
+export const resetRowZoom = () => (dispatch: Dispatch) => {
   dispatch({ type: RESET_ROW_ZOOM });
   dispatch(updateUrl());
 };
 
-function filterByFk(query, field, objectId) {
+function filterByFk(
+  query: Lib.Query,
+  field: DatasetColumn | Field,
+  objectId: ObjectId,
+) {
   const stageIndex = -1;
   const column = Lib.fromLegacyColumn(query, stageIndex, field);
   const filterClause =
@@ -72,6 +81,11 @@ export const followForeignKey = createThunkAction(
 
       const metadata = getMetadata(getState());
       const databaseId = new Question(card, metadata).databaseId();
+
+      if (!databaseId) {
+        return;
+      }
+
       const tableId = fk.origin.table.id;
       const metadataProvider = Lib.metadataProvider(databaseId, metadata);
       const table = Lib.tableOrCardMetadata(metadataProvider, tableId);
@@ -90,6 +104,11 @@ export const followForeignKey = createThunkAction(
   },
 );
 
+interface FKInfo {
+  status: number;
+  value: string | number | null;
+}
+
 export const LOAD_OBJECT_DETAIL_FK_REFERENCES =
   "metabase/qb/LOAD_OBJECT_DETAIL_FK_REFERENCES";
 export const loadObjectDetailFKReferences = createThunkAction(
@@ -105,13 +124,19 @@ export const loadObjectDetailFKReferences = createThunkAction(
         return null;
       }
 
-      const card = getCard(state);
+      const card: Card = getCard(state);
       const queryResult = getFirstQueryResult(state);
 
-      async function getFKCount(card, fk) {
+      async function getFKCount(
+        card: Card,
+        fk: ForeignKey,
+      ): Promise<FKInfo | undefined> {
         const metadata = getMetadata(getState());
         const databaseId = new Question(card, metadata).databaseId();
-        const tableId = fk.origin.table_id;
+        const tableId = fk.origin?.table_id;
+        if (!tableId || !databaseId || !fk.origin) {
+          return;
+        }
         const metadataProvider = Lib.metadataProvider(databaseId, metadata);
         const table = Lib.tableOrCardMetadata(metadataProvider, tableId);
         const baseQuery = Lib.queryFromTableOrCardMetadata(
@@ -124,7 +149,10 @@ export const loadObjectDetailFKReferences = createThunkAction(
           .setQuery(query)
           .datasetQuery();
 
-        const info = { status: 0, value: null };
+        const info: FKInfo = {
+          status: 0,
+          value: null,
+        };
 
         try {
           const result = await MetabaseApi.dataset(finalCard);
@@ -148,11 +176,12 @@ export const loadObjectDetailFKReferences = createThunkAction(
       // skipping that for now because it's easier to just run this each time
 
       // run a query on FK origin table where FK origin field = objectDetailIdValue
-      const fkReferences = {};
+      const fkReferences: Record<FieldId, FKInfo | undefined> = {};
       for (let i = 0; i < tableForeignKeys.length; i++) {
         const fk = tableForeignKeys[i];
         const info = await getFKCount(card, fk);
-        fkReferences[fk.origin.id] = info;
+
+        fkReferences[fk.origin_id] = info;
       }
 
       // It's possible that while we were running those queries, the object
@@ -171,7 +200,7 @@ export const CLEAR_OBJECT_DETAIL_FK_REFERENCES =
   "metabase/qb/CLEAR_OBJECT_DETAIL_FK_REFERENCES";
 
 export const viewNextObjectDetail = () => {
-  return (dispatch, getState) => {
+  return (dispatch: Dispatch, getState: GetState) => {
     if (getCanZoomNextRow(getState())) {
       const objectId = getNextRowPKValue(getState());
       dispatch(zoomInRow({ objectId }));
@@ -180,7 +209,7 @@ export const viewNextObjectDetail = () => {
 };
 
 export const viewPreviousObjectDetail = () => {
-  return (dispatch, getState) => {
+  return (dispatch: Dispatch, getState: GetState) => {
     if (getCanZoomPreviousRow(getState())) {
       const objectId = getPreviousRowPKValue(getState());
       dispatch(zoomInRow({ objectId }));
@@ -188,4 +217,5 @@ export const viewPreviousObjectDetail = () => {
   };
 };
 
-export const closeObjectDetail = () => dispatch => dispatch(resetRowZoom());
+export const closeObjectDetail = () => (dispatch: Dispatch) =>
+  dispatch(resetRowZoom());
diff --git a/frontend/src/metabase/query_builder/actions/querying.js b/frontend/src/metabase/query_builder/actions/querying.ts
similarity index 85%
rename from frontend/src/metabase/query_builder/actions/querying.js
rename to frontend/src/metabase/query_builder/actions/querying.ts
index 1db82f6a5cb911f2c59c80058a54f65ac620becf..a4e342d8281f8afbd7f00cd30a65517cd43af5e3 100644
--- a/frontend/src/metabase/query_builder/actions/querying.js
+++ b/frontend/src/metabase/query_builder/actions/querying.ts
@@ -8,7 +8,10 @@ import { getWhiteLabeledLoadingMessageFactory } from "metabase/selectors/whitela
 import { runQuestionQuery as apiRunQuestionQuery } from "metabase/services";
 import { getSensibleDisplays } from "metabase/visualizations";
 import * as Lib from "metabase-lib";
+import type Question from "metabase-lib/v1/Question";
 import { isAdHocModelOrMetricQuestion } from "metabase-lib/v1/metadata/utils/models";
+import type { Dataset } from "metabase-types/api";
+import type { Dispatch, GetState } from "metabase-types/store";
 
 import {
   getCard,
@@ -71,18 +74,23 @@ const loadCompleteUIControls = createThunkAction(
   },
 );
 
-export const runDirtyQuestionQuery = () => async (dispatch, getState) => {
-  const areResultsDirty = getIsResultDirty(getState());
-  const queryResults = getQueryResults(getState());
-  const hasResults = !!queryResults;
+export const runDirtyQuestionQuery =
+  () => async (dispatch: Dispatch, getState: GetState) => {
+    const areResultsDirty = getIsResultDirty(getState());
+    const queryResults = getQueryResults(getState());
 
-  if (hasResults && !areResultsDirty) {
-    const question = getQuestion(getState());
-    return dispatch(queryCompleted(question, queryResults));
-  }
+    if (queryResults && !areResultsDirty) {
+      const question = getQuestion(getState());
 
-  return dispatch(runQuestionQuery());
-};
+      if (!question) {
+        return;
+      }
+
+      return dispatch(queryCompleted(question, queryResults));
+    }
+
+    return dispatch(runQuestionQuery());
+  };
 
 /**
  * Queries the result for the currently active question or alternatively for the card question provided in `overrideWithQuestion`.
@@ -93,8 +101,12 @@ export const runQuestionQuery = ({
   shouldUpdateUrl = true,
   ignoreCache = false,
   overrideWithQuestion = null,
+}: {
+  shouldUpdateUrl?: boolean;
+  ignoreCache?: boolean;
+  overrideWithQuestion?: Question | null;
 } = {}) => {
-  return async (dispatch, getState) => {
+  return async (dispatch: Dispatch, getState: GetState) => {
     dispatch(loadStartUIControls());
 
     const question = overrideWithQuestion
@@ -102,6 +114,10 @@ export const runQuestionQuery = ({
       : getQuestion(getState());
     const originalQuestion = getOriginalQuestion(getState());
 
+    if (!question) {
+      return;
+    }
+
     const isCardDirty = originalQuestion
       ? question.isDirtyComparedToWithoutParameters(originalQuestion) ||
         question.id() == null
@@ -166,8 +182,8 @@ export const CLEAR_QUERY_RESULT = "metabase/query_builder/CLEAR_QUERY_RESULT";
 export const clearQueryResult = createAction(CLEAR_QUERY_RESULT);
 
 export const QUERY_COMPLETED = "metabase/qb/QUERY_COMPLETED";
-export const queryCompleted = (question, queryResults) => {
-  return async (dispatch, getState) => {
+export const queryCompleted = (question: Question, queryResults: Dataset[]) => {
+  return async (dispatch: Dispatch, getState: GetState) => {
     const [{ data, error }] = queryResults;
     const prevCard = getCard(getState());
     const { data: prevData, error: prevError } =
@@ -222,14 +238,14 @@ export const queryErrored = createThunkAction(
       if (error && error.isCancelled) {
         return null;
       } else {
-        return { error: error, duration: new Date() - startTime };
+        return { error: error, duration: Date.now() - startTime };
       }
     };
   },
 );
 
 export const CANCEL_QUERY = "metabase/qb/CANCEL_QUERY";
-export const cancelQuery = () => (dispatch, getState) => {
+export const cancelQuery = () => (dispatch: Dispatch, getState: GetState) => {
   const isRunning = getIsRunning(getState());
   if (isRunning) {
     const { cancelQueryDeferred } = getState().qb;
diff --git a/frontend/src/metabase/query_builder/actions/timelines.js b/frontend/src/metabase/query_builder/actions/timelines.ts
similarity index 77%
rename from frontend/src/metabase/query_builder/actions/timelines.js
rename to frontend/src/metabase/query_builder/actions/timelines.ts
index ddcbd2eb26929237b7aca5bdf4ea693eb9e99a72..46cb883db3428e9f18d48388dc371285e18f3e2c 100644
--- a/frontend/src/metabase/query_builder/actions/timelines.js
+++ b/frontend/src/metabase/query_builder/actions/timelines.ts
@@ -1,5 +1,8 @@
 import { createAction } from "redux-actions";
 
+import type { CollectionId, Timeline } from "metabase-types/api";
+import type { Dispatch, GetState } from "metabase-types/store";
+
 import { getFetchedTimelines } from "../selectors";
 
 export const SELECT_TIMELINE_EVENTS = "metabase/qb/SELECT_TIMELINE_EVENTS";
@@ -15,8 +18,9 @@ export const SHOW_TIMELINE_EVENTS = "metabase/qb/SHOW_TIMELINE_EVENTS";
 export const showTimelineEvents = createAction(SHOW_TIMELINE_EVENTS);
 
 export const showTimelinesForCollection =
-  collectionId => (dispatch, getState) => {
-    const fetchedTimelines = getFetchedTimelines(getState());
+  (collectionId?: CollectionId | null) =>
+  (dispatch: Dispatch, getState: GetState) => {
+    const fetchedTimelines: Timeline[] = getFetchedTimelines(getState());
     const collectionTimelines = collectionId
       ? fetchedTimelines.filter(t => t.collection_id === collectionId)
       : fetchedTimelines.filter(t => t.collection_id == null);
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.unit.spec.tsx b/frontend/src/metabase/query_builder/components/QueryVisualization.unit.spec.tsx
index 530fb97e7aab70ceb8defc90ff239ce442c25879..0640d9875e5e2a6d7e690cb2190da29e5820201a 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.unit.spec.tsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.unit.spec.tsx
@@ -4,7 +4,7 @@ import { PLUGIN_SELECTORS } from "metabase/plugins";
 import { VisualizationRunningState } from "./QueryVisualization";
 
 type SetupOpts = {
-  customMessage?: (isSlow: boolean) => string;
+  customMessage?: (isSlow?: boolean) => string;
 };
 
 function setup({ customMessage }: SetupOpts = {}) {
@@ -31,7 +31,7 @@ describe("VisualizationRunningState", () => {
   });
 
   it("should only render the custom loading message when it was customized", async () => {
-    const customMessage = (isSlow: boolean) =>
+    const customMessage = (isSlow?: boolean) =>
       isSlow ? `Custom message (slow)...` : `Custom message...`;
 
     setup({ customMessage });
diff --git a/frontend/src/metabase/redux/app.ts b/frontend/src/metabase/redux/app.ts
index 135c48d74ee6f2e7c7ec0f6fd52301787dc8f94b..a2dabff407f8b7a8f149e3fc1994ab60d5145477 100644
--- a/frontend/src/metabase/redux/app.ts
+++ b/frontend/src/metabase/redux/app.ts
@@ -49,7 +49,8 @@ interface IOpenUrlOptions {
 }
 
 export const openUrl =
-  (url: string, options: IOpenUrlOptions) => (dispatch: Dispatch) => {
+  (url: string, options: IOpenUrlOptions = {}) =>
+  (dispatch: Dispatch) => {
     if (shouldOpenInBlankWindow(url, options)) {
       openInBlankWindow(url);
     } else {