From d39dce81eef1a5d397e06285576002293ab6570a Mon Sep 17 00:00:00 2001
From: Anton Kulyk <kuliks.anton@gmail.com>
Date: Tue, 13 Dec 2022 10:15:34 +0000
Subject: [PATCH] Convert drill code to TypeScript (#27165)

* Extend `field` types

* Add series types

* Fix `FieldId` type import

* Add `metabase/visualizations/types`

* Add drill types

* Specify `Question.prototype.pivot` return type

* Update `AutomaticDashboardDrill`

* Update `ColumnFilterDrill`

* Update `CompareToRestDrill`

* Update `DashboardClickDrill`

* Update `DistributionDrill`

* Update `ForeignKeyDrill`

* Update `ObjectDetailDrill`

* Update `PivotByCategoryDrill`

* Update `PivotByLocationDrill`

* Update `PivotByTimeDrill`

* Update `QuickFilterDrill`

* Update `SummarizeColumnByTimeDrill`

* Update `SummarizeColumnDrill`

* Update `UnderlyingRecordsDrill`

* Update `ZoomDrill`

* Update `FormatDrill` (former `FormatAction`)

* Update `SortDrill` (former `SortAction`)

* Update `NativeDrillFallback`

* Fix types

* Fix handling missing `clicked` object

* Simplify drill type hierarchy
---
 frontend/src/metabase-lib/Question.ts         |  5 +-
 frontend/src/metabase-lib/references.ts       |  2 +-
 frontend/src/metabase-types/api/dataset.ts    | 20 +++-
 frontend/src/metabase-types/api/field.ts      | 57 +++++++++--
 .../src/metabase-types/api/mocks/field.ts     | 17 +++-
 frontend/src/metabase-types/api/query.ts      |  2 +-
 ...rdDrill.jsx => AutomaticDashboardDrill.ts} |  7 +-
 .../components/drill/ColumnFilterDrill.jsx    | 41 --------
 .../components/drill/ColumnFilterDrill.tsx    | 49 +++++++++
 ...reToRestDrill.js => CompareToRestDrill.ts} |  7 +-
 ...ClickDrill.jsx => DashboardClickDrill.tsx} | 28 +++++-
 ...ributionDrill.jsx => DistributionDrill.ts} |  7 +-
 ...ForeignKeyDrill.jsx => ForeignKeyDrill.ts} | 25 +++--
 .../modes/components/drill/FormatAction.jsx   | 75 --------------
 .../drill/FormatDrill/FormatDrill.styled.tsx  |  7 ++
 .../drill/FormatDrill/FormatDrill.tsx         | 78 +++++++++++++++
 .../components/drill/FormatDrill/index.ts     |  1 +
 .../NativeDrillFallback.styled.tsx            |  1 -
 .../NativeDrillFallback.tsx                   | 13 +--
 ...ctDetailDrill.jsx => ObjectDetailDrill.ts} | 29 +++++-
 .../components/drill/PivotByCategoryDrill.jsx | 45 ---------
 .../components/drill/PivotByCategoryDrill.tsx | 58 +++++++++++
 .../components/drill/PivotByLocationDrill.jsx | 45 ---------
 .../components/drill/PivotByLocationDrill.tsx | 58 +++++++++++
 .../components/drill/PivotByTimeDrill.jsx     | 43 --------
 .../components/drill/PivotByTimeDrill.tsx     | 54 ++++++++++
 ...ckFilterDrill.jsx => QuickFilterDrill.tsx} | 14 +--
 .../drill/{SortAction.jsx => SortDrill.ts}    | 11 ++-
 ...Drill.js => SummarizeColumnByTimeDrill.ts} |  9 +-
 .../components/drill/SummarizeColumnDrill.js  | 42 --------
 .../components/drill/SummarizeColumnDrill.ts  | 71 +++++++++++++
 ...rdsDrill.jsx => UnderlyingRecordsDrill.ts} |  9 +-
 .../drill/{ZoomDrill.jsx => ZoomDrill.ts}     |  9 +-
 .../modes/components/modes/DefaultMode.jsx    |  8 +-
 frontend/src/metabase/modes/types.ts          | 99 +++++++++++++++++++
 ...reakoutPopover.jsx => BreakoutPopover.tsx} | 25 ++++-
 frontend/src/metabase/visualizations/types.ts |  8 ++
 37 files changed, 711 insertions(+), 368 deletions(-)
 rename frontend/src/metabase/modes/components/drill/{AutomaticDashboardDrill.jsx => AutomaticDashboardDrill.ts} (79%)
 delete mode 100644 frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx
 create mode 100644 frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx
 rename frontend/src/metabase/modes/components/drill/{CompareToRestDrill.js => CompareToRestDrill.ts} (80%)
 rename frontend/src/metabase/modes/components/drill/{DashboardClickDrill.jsx => DashboardClickDrill.tsx} (70%)
 rename frontend/src/metabase/modes/components/drill/{DistributionDrill.jsx => DistributionDrill.ts} (76%)
 rename frontend/src/metabase/modes/components/drill/{ForeignKeyDrill.jsx => ForeignKeyDrill.ts} (51%)
 delete mode 100644 frontend/src/metabase/modes/components/drill/FormatAction.jsx
 create mode 100644 frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx
 create mode 100644 frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx
 create mode 100644 frontend/src/metabase/modes/components/drill/FormatDrill/index.ts
 rename frontend/src/metabase/modes/components/drill/{ObjectDetailDrill.jsx => ObjectDetailDrill.ts} (63%)
 delete mode 100644 frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx
 create mode 100644 frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx
 delete mode 100644 frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx
 create mode 100644 frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx
 delete mode 100644 frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx
 create mode 100644 frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx
 rename frontend/src/metabase/modes/components/drill/{QuickFilterDrill.jsx => QuickFilterDrill.tsx} (69%)
 rename frontend/src/metabase/modes/components/drill/{SortAction.jsx => SortDrill.ts} (78%)
 rename frontend/src/metabase/modes/components/drill/{SummarizeColumnByTimeDrill.js => SummarizeColumnByTimeDrill.ts} (75%)
 delete mode 100644 frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js
 create mode 100644 frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts
 rename frontend/src/metabase/modes/components/drill/{UnderlyingRecordsDrill.jsx => UnderlyingRecordsDrill.ts} (84%)
 rename frontend/src/metabase/modes/components/drill/{ZoomDrill.jsx => ZoomDrill.ts} (77%)
 create mode 100644 frontend/src/metabase/modes/types.ts
 rename frontend/src/metabase/query_builder/components/{BreakoutPopover.jsx => BreakoutPopover.tsx} (59%)
 create mode 100644 frontend/src/metabase/visualizations/types.ts

diff --git a/frontend/src/metabase-lib/Question.ts b/frontend/src/metabase-lib/Question.ts
index 51ed8d30095..fa25a650a0b 100644
--- a/frontend/src/metabase-lib/Question.ts
+++ b/frontend/src/metabase-lib/Question.ts
@@ -527,7 +527,10 @@ class QuestionInner {
     return filter(this, operator, column, value) || this;
   }
 
-  pivot(breakouts = [], dimensions = []): Question {
+  pivot(
+    breakouts: (Breakout | Dimension | Field)[] = [],
+    dimensions = [],
+  ): Question {
     return pivot(this, breakouts, dimensions) || this;
   }
 
diff --git a/frontend/src/metabase-lib/references.ts b/frontend/src/metabase-lib/references.ts
index bdd7a4cc122..f54c9a4a97d 100644
--- a/frontend/src/metabase-lib/references.ts
+++ b/frontend/src/metabase-lib/references.ts
@@ -9,7 +9,7 @@ import {
   FieldReference,
   ReferenceOptions,
   TemplateTagReference,
-} from "metabase-types/api/query";
+} from "metabase-types/api";
 
 export const isFieldReference = (mbql: any): mbql is FieldReference => {
   return Array.isArray(mbql) && mbql.length === 3 && mbql[0] === "field";
diff --git a/frontend/src/metabase-types/api/dataset.ts b/frontend/src/metabase-types/api/dataset.ts
index 14b1ccbf8cd..22ae14e5d6a 100644
--- a/frontend/src/metabase-types/api/dataset.ts
+++ b/frontend/src/metabase-types/api/dataset.ts
@@ -1,4 +1,6 @@
+import { Card } from "./card";
 import { DatabaseId } from "./database";
+import { FieldId } from "./field";
 import { DatetimeUnit, DimensionReference } from "./query";
 import { DownloadPermission } from "./permissions";
 
@@ -6,10 +8,10 @@ export type RowValue = string | number | null | boolean;
 export type RowValues = RowValue[];
 
 export interface DatasetColumn {
-  id?: number;
+  id?: FieldId;
+  name: string;
   display_name: string;
   source: string;
-  name: string;
   // FIXME: this prop does not come from API
   remapped_to_column?: DatasetColumn;
   unit?: DatetimeUnit;
@@ -41,3 +43,17 @@ export interface Dataset {
 export interface NativeQueryForm {
   query: string;
 }
+
+export type SingleSeries = {
+  card: Card;
+  data: DatasetData;
+  error_type?: string;
+  error?: {
+    status: number; // HTTP status code
+    data?: string;
+  };
+};
+
+export type RawSeries = SingleSeries[];
+export type TransformedSeries = RawSeries & { _raw: Series };
+export type Series = RawSeries | TransformedSeries;
diff --git a/frontend/src/metabase-types/api/field.ts b/frontend/src/metabase-types/api/field.ts
index 5b065268859..851383d7de0 100644
--- a/frontend/src/metabase-types/api/field.ts
+++ b/frontend/src/metabase-types/api/field.ts
@@ -1,3 +1,8 @@
+import { RowValue } from "./dataset";
+import { TableId } from "./table";
+
+export type FieldId = number;
+
 export type TextFieldFingerprint = {
   "average-length": number;
   "percent-email": number;
@@ -32,19 +37,51 @@ export interface FieldFingerprint {
   };
 }
 
+export type FieldVisibilityType =
+  | "details-only"
+  | "hidden"
+  | "normal"
+  | "retired";
+
+type HumanReadableFieldValue = string;
+type FieldValue = [RowValue] | [RowValue, HumanReadableFieldValue];
+
+export type FieldDimension = {
+  name: string;
+};
+
 export interface Field {
-  id?: number;
-  dimensions?: FieldDimension;
-  display_name: string;
-  table_id: number | string;
+  id?: FieldId;
+  table_id: TableId;
+
   name: string;
-  base_type: string;
+  display_name: string;
   description: string | null;
-  nfc_path: string[] | null;
 
+  base_type: string;
+  effective_type?: string;
+  semantic_type: string;
+
+  active: boolean;
+  visibility_type: FieldVisibilityType;
+  preview_display: boolean;
+  position: number;
+
+  parent_id?: FieldId;
+  fk_target_field_id?: FieldId;
+  values?: FieldValue[];
+  dimensions?: FieldDimension;
+
+  max_value?: number;
+  min_value?: number;
+
+  caveats?: string | null;
+  points_of_interest?: string;
+
+  nfc_path: string[] | null;
   fingerprint?: FieldFingerprint;
-}
 
-export type FieldDimension = {
-  name: string;
-};
+  last_analyzed: string;
+  created_at: string;
+  updated_at: string;
+}
diff --git a/frontend/src/metabase-types/api/mocks/field.ts b/frontend/src/metabase-types/api/mocks/field.ts
index de75b99377e..26ae847fe99 100644
--- a/frontend/src/metabase-types/api/mocks/field.ts
+++ b/frontend/src/metabase-types/api/mocks/field.ts
@@ -2,11 +2,24 @@ import { Field } from "metabase-types/api";
 
 export const createMockField = (opts?: Partial<Field>): Field => ({
   id: 1,
+
+  name: "mock_field",
   display_name: "Mock Field",
+  description: null,
+
   table_id: 1,
-  name: "mock_field",
+
   base_type: "type/Text",
-  description: null,
+  semantic_type: "type/Text",
+
+  active: true,
+  visibility_type: "normal",
+  preview_display: true,
+  position: 1,
   nfc_path: null,
+
+  last_analyzed: new Date().toISOString(),
+  created_at: new Date().toISOString(),
+  updated_at: new Date().toISOString(),
   ...opts,
 });
diff --git a/frontend/src/metabase-types/api/query.ts b/frontend/src/metabase-types/api/query.ts
index 0b3007fb365..0a150f079e4 100644
--- a/frontend/src/metabase-types/api/query.ts
+++ b/frontend/src/metabase-types/api/query.ts
@@ -1,4 +1,5 @@
 import { DatabaseId } from "./database";
+import { FieldId } from "./field";
 import { TableId } from "./table";
 
 export interface StructuredQuery {
@@ -74,7 +75,6 @@ export type ReferenceOptionsKeys =
   | "temporal-unit"
   | "binning";
 
-export type FieldId = number;
 export type ColumnName = string;
 export type FieldReference = [
   "field",
diff --git a/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.jsx b/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.ts
similarity index 79%
rename from frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.jsx
rename to frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.ts
index 6bc59cb08f7..53d3e63eabb 100644
--- a/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/AutomaticDashboardDrill.ts
@@ -4,8 +4,9 @@ import {
   automaticDashboardDrill,
   automaticDashboardDrillUrl,
 } from "metabase-lib/queries/drills/automatic-dashboard-drill";
+import type { Drill } from "../../types";
 
-export default ({ question, clicked }) => {
+const AutomaticDashboardDrill: Drill = ({ question, clicked }) => {
   const enableXrays = MetabaseSettings.get("enable-xrays");
   if (!automaticDashboardDrill({ question, clicked, enableXrays })) {
     return [];
@@ -14,11 +15,13 @@ export default ({ question, clicked }) => {
   return [
     {
       name: "exploratory-dashboard",
+      title: t`X-ray`,
       section: "auto",
       icon: "bolt",
       buttonType: "token",
-      title: t`X-ray`,
       url: () => automaticDashboardDrillUrl({ question, clicked }),
     },
   ];
 };
+
+export default AutomaticDashboardDrill;
diff --git a/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx b/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx
deleted file mode 100644
index 7ab0aa6777f..00000000000
--- a/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t } from "ttag";
-
-import FilterPopover from "metabase/query_builder/components/filters/FilterPopover";
-import { columnFilterDrill } from "metabase-lib/queries/drills/column-filter-drill";
-
-export default function ColumnFilterDrill({ question, clicked }) {
-  const drill = columnFilterDrill({ question, clicked });
-  if (!drill) {
-    return [];
-  }
-
-  const { query, initialFilter } = drill;
-
-  return [
-    {
-      name: "filter-column",
-      section: "summarize",
-      title: t`Filter by this column`,
-      buttonType: "horizontal",
-      icon: "filter",
-      // eslint-disable-next-line react/display-name
-      popover: ({ onChangeCardAndRun, onResize, onClose }) => (
-        <FilterPopover
-          query={query}
-          filter={initialFilter}
-          onClose={onClose}
-          onResize={onResize}
-          onChangeFilter={filter => {
-            const nextCard = query.filter(filter).question().card();
-            onChangeCardAndRun({ nextCard });
-            onClose();
-          }}
-          showFieldPicker={false}
-          isNew={true}
-        />
-      ),
-    },
-  ];
-}
diff --git a/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx b/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx
new file mode 100644
index 00000000000..7b07b8cad9f
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/ColumnFilterDrill.tsx
@@ -0,0 +1,49 @@
+import React from "react";
+import { t } from "ttag";
+
+import FilterPopover from "metabase/query_builder/components/filters/FilterPopover";
+import { columnFilterDrill } from "metabase-lib/queries/drills/column-filter-drill";
+
+import type { Drill, ClickActionPopoverProps } from "../../types";
+
+const ColumnFilterDrill: Drill = ({ question, clicked }) => {
+  const drill = columnFilterDrill({ question, clicked });
+  if (!drill) {
+    return [];
+  }
+
+  const { query, initialFilter } = drill;
+
+  const ColumnFilterDrillPopover = ({
+    onChangeCardAndRun,
+    onResize,
+    onClose,
+  }: ClickActionPopoverProps) => (
+    <FilterPopover
+      isNew
+      query={query}
+      filter={initialFilter}
+      showFieldPicker={false}
+      onClose={onClose}
+      onResize={onResize}
+      onChangeFilter={filter => {
+        const nextCard = query.filter(filter).question().card();
+        onChangeCardAndRun({ nextCard });
+        onClose();
+      }}
+    />
+  );
+
+  return [
+    {
+      name: "filter-column",
+      section: "summarize",
+      title: t`Filter by this column`,
+      buttonType: "horizontal",
+      icon: "filter",
+      popover: ColumnFilterDrillPopover,
+    },
+  ];
+};
+
+export default ColumnFilterDrill;
diff --git a/frontend/src/metabase/modes/components/drill/CompareToRestDrill.js b/frontend/src/metabase/modes/components/drill/CompareToRestDrill.ts
similarity index 80%
rename from frontend/src/metabase/modes/components/drill/CompareToRestDrill.js
rename to frontend/src/metabase/modes/components/drill/CompareToRestDrill.ts
index 85626a1cc26..76ab129fa62 100644
--- a/frontend/src/metabase/modes/components/drill/CompareToRestDrill.js
+++ b/frontend/src/metabase/modes/components/drill/CompareToRestDrill.ts
@@ -5,8 +5,9 @@ import {
   compareToRestDrill,
   compareToRestDrillUrl,
 } from "metabase-lib/queries/drills/compare-to-rest-drill";
+import type { Drill } from "../../types";
 
-export default ({ question, clicked }) => {
+const CompareToRestDrill: Drill = ({ question, clicked }) => {
   const enableXrays = MetabaseSettings.get("enable-xrays");
   if (!compareToRestDrill({ question, clicked, enableXrays })) {
     return [];
@@ -15,11 +16,13 @@ export default ({ question, clicked }) => {
   return [
     {
       name: "compare-dashboard",
+      title: t`Compare to the rest`,
       section: "auto",
       icon: "bolt",
       buttonType: "token",
-      title: t`Compare to the rest`,
       url: () => compareToRestDrillUrl({ question, clicked }),
     },
   ];
 };
+
+export default CompareToRestDrill;
diff --git a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.tsx
similarity index 70%
rename from frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
rename to frontend/src/metabase/modes/components/drill/DashboardClickDrill.tsx
index 7ed37820384..53552250ab5 100644
--- a/frontend/src/metabase/modes/components/drill/DashboardClickDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/DashboardClickDrill.tsx
@@ -3,6 +3,10 @@ import {
   setOrUnsetParameterValues,
   setParameterValue,
 } from "metabase/dashboard/actions";
+
+import type { ReduxAction } from "metabase-types/store";
+import type Question from "metabase-lib/Question";
+
 import {
   getDashboardDrillLinkUrl,
   getDashboardDrillPageUrl,
@@ -12,7 +16,21 @@ import {
   getDashboardDrillUrl,
 } from "metabase-lib/queries/drills/dashboard-click-drill";
 
-function getAction(type, question, clicked) {
+import type { ClickAction, ClickObject, Drill } from "../../types";
+
+type DashboardDrillType =
+  | "link-url"
+  | "question-url"
+  | "page-url"
+  | "dashboard-url"
+  | "dashboard-filter"
+  | "dashboard-reset";
+
+function getAction(
+  type: DashboardDrillType,
+  question: Question,
+  clicked: ClickObject,
+): Partial<ClickAction> {
   switch (type) {
     case "link-url":
       return {
@@ -24,7 +42,9 @@ function getAction(type, question, clicked) {
         url: () => getDashboardDrillQuestionUrl(question, clicked),
       };
     case "page-url":
-      return { action: () => push(getDashboardDrillPageUrl(clicked)) };
+      return {
+        action: () => push(getDashboardDrillPageUrl(clicked)) as ReduxAction,
+      };
     case "dashboard-url":
       return { url: () => getDashboardDrillUrl(clicked) };
     case "dashboard-filter":
@@ -46,7 +66,7 @@ function getAction(type, question, clicked) {
   }
 }
 
-export default ({ question, clicked }) => {
+const DashboardClickDrill: Drill = ({ question, clicked = {} }) => {
   const type = getDashboardDrillType(clicked);
   if (!type) {
     return [];
@@ -60,3 +80,5 @@ export default ({ question, clicked }) => {
     },
   ];
 };
+
+export default DashboardClickDrill;
diff --git a/frontend/src/metabase/modes/components/drill/DistributionDrill.jsx b/frontend/src/metabase/modes/components/drill/DistributionDrill.ts
similarity index 76%
rename from frontend/src/metabase/modes/components/drill/DistributionDrill.jsx
rename to frontend/src/metabase/modes/components/drill/DistributionDrill.ts
index 18015b2472f..fbd5afc862f 100644
--- a/frontend/src/metabase/modes/components/drill/DistributionDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/DistributionDrill.ts
@@ -3,8 +3,9 @@ import {
   distributionDrill,
   distributionDrillQuestion,
 } from "metabase-lib/queries/drills/distribution-drill";
+import type { Drill } from "../../types";
 
-export default ({ question, clicked }) => {
+const DistributionDrill: Drill = ({ question, clicked }) => {
   if (!distributionDrill({ question, clicked })) {
     return [];
   }
@@ -13,10 +14,12 @@ export default ({ question, clicked }) => {
     {
       name: "distribution",
       title: t`Distribution`,
-      buttonType: "horizontal",
       section: "summarize",
       icon: "bar",
+      buttonType: "horizontal",
       question: () => distributionDrillQuestion({ question, clicked }),
     },
   ];
 };
+
+export default DistributionDrill;
diff --git a/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.jsx b/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.ts
similarity index 51%
rename from frontend/src/metabase/modes/components/drill/ForeignKeyDrill.jsx
rename to frontend/src/metabase/modes/components/drill/ForeignKeyDrill.ts
index a7911808a95..fe5ec25dc71 100644
--- a/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/ForeignKeyDrill.ts
@@ -4,8 +4,9 @@ import {
   foreignKeyDrill,
   foreignKeyDrillQuestion,
 } from "metabase-lib/queries/drills/foreign-key-drill";
+import type { Drill } from "../../types";
 
-export default function ForeignKeyDrill({ question, clicked }) {
+const ForeignKeyDrill: Drill = ({ question, clicked }) => {
   const drill = foreignKeyDrill({ question, clicked });
   if (!drill) {
     return [];
@@ -15,12 +16,16 @@ export default function ForeignKeyDrill({ question, clicked }) {
   const columnTitle = singularize(columnName);
   const tableTitle = pluralize(tableName);
 
-  return {
-    name: "view-fks",
-    section: "standalone_filter",
-    buttonType: "horizontal",
-    icon: "filter",
-    title: t`View this ${columnTitle}'s ${tableTitle}`,
-    question: () => foreignKeyDrillQuestion({ question, clicked }),
-  };
-}
+  return [
+    {
+      name: "view-fks",
+      title: t`View this ${columnTitle}'s ${tableTitle}`,
+      section: "standalone_filter",
+      icon: "filter",
+      buttonType: "horizontal",
+      question: () => foreignKeyDrillQuestion({ question, clicked }),
+    },
+  ];
+};
+
+export default ForeignKeyDrill;
diff --git a/frontend/src/metabase/modes/components/drill/FormatAction.jsx b/frontend/src/metabase/modes/components/drill/FormatAction.jsx
deleted file mode 100644
index 9ebfb408209..00000000000
--- a/frontend/src/metabase/modes/components/drill/FormatAction.jsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React from "react";
-import styled from "@emotion/styled";
-
-/* eslint-disable react/prop-types */
-import { t } from "ttag";
-
-import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
-import ChartSettingsWidget from "metabase/visualizations/components/ChartSettingsWidget";
-import { updateSettings } from "metabase/visualizations/lib/settings";
-import { getColumnKey } from "metabase-lib/queries/utils/get-column-key";
-
-export default ({ question, clicked }) => {
-  if (
-    !clicked ||
-    clicked.value !== undefined ||
-    !clicked.column ||
-    !question.query().isEditable()
-  ) {
-    return [];
-  }
-  const { column } = clicked;
-
-  return [
-    {
-      name: "formatting",
-      title: "Column formatting",
-      section: "sort",
-      buttonType: "formatting",
-      icon: "gear",
-      tooltip: t`Column formatting`,
-      popoverProps: {
-        placement: "right-end",
-        offset: [0, 20],
-      },
-      popover: function FormatPopover({ series, onChange }) {
-        const handleChangeSettings = changedSettings => {
-          onChange(
-            updateSettings(
-              series[0].card.visualization_settings,
-              changedSettings,
-            ),
-          );
-        };
-
-        const columnSettingsWidget = getSettingsWidgetsForSeries(
-          series,
-          handleChangeSettings,
-          false,
-        ).find(widget => widget.id === "column_settings");
-
-        return (
-          <PopoverRoot>
-            <ChartSettingsWidget
-              key={columnSettingsWidget.id}
-              {...{
-                ...columnSettingsWidget,
-                props: {
-                  ...columnSettingsWidget.props,
-                  initialKey: getColumnKey(column),
-                },
-              }}
-              hidden={false}
-            />
-          </PopoverRoot>
-        );
-      },
-    },
-  ];
-};
-
-const PopoverRoot = styled.div`
-  padding-top: 1.5rem;
-  max-height: 600px;
-  overflow-y: auto;
-`;
diff --git a/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx
new file mode 100644
index 00000000000..31687267f55
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.styled.tsx
@@ -0,0 +1,7 @@
+import styled from "@emotion/styled";
+
+export const PopoverRoot = styled.div`
+  padding-top: 1.5rem;
+  max-height: 600px;
+  overflow-y: auto;
+`;
diff --git a/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx
new file mode 100644
index 00000000000..53ce0731d47
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/FormatDrill/FormatDrill.tsx
@@ -0,0 +1,78 @@
+import React from "react";
+import { t } from "ttag";
+
+import { getSettingsWidgetsForSeries } from "metabase/visualizations/lib/settings/visualization";
+import ChartSettingsWidget from "metabase/visualizations/components/ChartSettingsWidget";
+import { updateSettings } from "metabase/visualizations/lib/settings";
+
+import type { VisualizationSettings } from "metabase-types/api";
+
+import { getColumnKey } from "metabase-lib/queries/utils/get-column-key";
+
+import type { ClickActionPopoverProps, Drill } from "../../../types";
+import { PopoverRoot } from "./FormatDrill.styled";
+
+const FormatDrill: Drill = ({ question, clicked }) => {
+  if (
+    !clicked ||
+    clicked.value !== undefined ||
+    !clicked.column ||
+    !question.query().isEditable()
+  ) {
+    return [];
+  }
+
+  const { column } = clicked;
+
+  const FormatPopover = ({ series, onChange }: ClickActionPopoverProps) => {
+    const handleChangeSettings = (settings: VisualizationSettings) => {
+      onChange(updateSettings(series[0].card.visualization_settings, settings));
+    };
+
+    const widgets = getSettingsWidgetsForSeries(
+      series,
+      handleChangeSettings,
+      false,
+    );
+
+    const columnSettingsWidget = widgets.find(
+      widget => widget.id === "column_settings",
+    );
+
+    const extraProps = {
+      ...columnSettingsWidget,
+      props: {
+        ...columnSettingsWidget.props,
+        initialKey: getColumnKey(column),
+      },
+    };
+
+    return (
+      <PopoverRoot>
+        <ChartSettingsWidget
+          {...extraProps}
+          key={columnSettingsWidget.id}
+          hidden={false}
+        />
+      </PopoverRoot>
+    );
+  };
+
+  return [
+    {
+      name: "formatting",
+      title: t`Column formatting`,
+      section: "sort",
+      buttonType: "formatting",
+      icon: "gear",
+      tooltip: t`Column formatting`,
+      popoverProps: {
+        placement: "right-end",
+        offset: [0, 20],
+      },
+      popover: FormatPopover,
+    },
+  ];
+};
+
+export default FormatDrill;
diff --git a/frontend/src/metabase/modes/components/drill/FormatDrill/index.ts b/frontend/src/metabase/modes/components/drill/FormatDrill/index.ts
new file mode 100644
index 00000000000..57a961ff48c
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/FormatDrill/index.ts
@@ -0,0 +1 @@
+export { default } from "./FormatDrill";
diff --git a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx
index 44a05f68f3b..42de24d0ec4 100644
--- a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx
+++ b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.styled.tsx
@@ -1,6 +1,5 @@
 import styled from "@emotion/styled";
 import { color } from "metabase/lib/colors";
-import Icon from "metabase/components/Icon";
 import ExternalLink from "metabase/core/components/ExternalLink";
 
 export const DrillRoot = styled.div`
diff --git a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx
index bc950209e17..6b573adf5a2 100644
--- a/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx
+++ b/frontend/src/metabase/modes/components/drill/NativeDrillFallback/NativeDrillFallback.tsx
@@ -1,21 +1,18 @@
 import React from "react";
 import { t } from "ttag";
+import Icon from "metabase/components/Icon";
 import MetabaseSettings from "metabase/lib/settings";
 import { getEngineNativeType } from "metabase/lib/engine";
-import Icon from "metabase/components/Icon";
-import Question from "metabase-lib/Question";
 import { nativeDrillFallback } from "metabase-lib/queries/drills/native-drill-fallback";
+
+import type { ClickAction, Drill } from "../../../types";
 import {
   DrillLearnLink,
   DrillMessage,
   DrillRoot,
 } from "./NativeDrillFallback.styled";
 
-interface NativeDrillFallbackProps {
-  question: Question;
-}
-
-const NativeDrillFallback = ({ question }: NativeDrillFallbackProps) => {
+const NativeDrillFallback: Drill = ({ question }) => {
   const drill = nativeDrillFallback({ question });
   if (!drill) {
     return [];
@@ -43,7 +40,7 @@ const NativeDrillFallback = ({ question }: NativeDrillFallbackProps) => {
           </DrillLearnLink>
         </DrillRoot>
       ),
-    },
+    } as ClickAction,
   ];
 };
 
diff --git a/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.ts
similarity index 63%
rename from frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx
rename to frontend/src/metabase/modes/components/drill/ObjectDetailDrill.ts
index b2a974249bc..f3e9e107854 100644
--- a/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/ObjectDetailDrill.ts
@@ -1,12 +1,25 @@
 import { t } from "ttag";
 import { zoomInRow } from "metabase/query_builder/actions";
+
+import type { RowValue } from "metabase-types/api";
+import type Question from "metabase-lib/Question";
+
 import {
   objectDetailDrill,
   objectDetailFKDrillQuestion,
   objectDetailPKDrillQuestion,
 } from "metabase-lib/queries/drills/object-detail-drill";
 
-function getAction({ question, clicked, type, objectId }) {
+import type { Drill, DrillOptions } from "../../types";
+
+type DrillType = "pk" | "fk" | "zoom" | "dashboard";
+
+function getAction({
+  question,
+  clicked,
+  type,
+  objectId,
+}: DrillOptions & { question: Question; type: DrillType; objectId: RowValue }) {
   switch (type) {
     case "pk":
       return {
@@ -23,7 +36,13 @@ function getAction({ question, clicked, type, objectId }) {
   }
 }
 
-function getActionExtraData({ objectId, hasManyPKColumns }) {
+function getActionExtraData({
+  objectId,
+  hasManyPKColumns,
+}: {
+  objectId: RowValue;
+  hasManyPKColumns: boolean;
+}) {
   if (!hasManyPKColumns) {
     return {
       extra: () => ({ objectId }),
@@ -31,7 +50,7 @@ function getActionExtraData({ objectId, hasManyPKColumns }) {
   }
 }
 
-export default ({ question, clicked }) => {
+const ObjectDetailDrill: Drill = ({ question, clicked }) => {
   const drill = objectDetailDrill({ question, clicked });
   if (!drill) {
     return [];
@@ -47,8 +66,10 @@ export default ({ question, clicked }) => {
       buttonType: "horizontal",
       icon: "document",
       default: true,
-      ...getAction({ question, clicked, type, objectId }),
+      ...getAction({ question, clicked, type: type as DrillType, objectId }),
       ...getActionExtraData({ objectId, hasManyPKColumns }),
     },
   ];
 };
+
+export default ObjectDetailDrill;
diff --git a/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx b/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx
deleted file mode 100644
index 78bfe23d098..00000000000
--- a/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t, jt } from "ttag";
-import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
-import { pivotByCategoryDrill } from "metabase-lib/queries/drills/pivot-drill";
-
-export default ({ question, clicked }) => {
-  const drill = pivotByCategoryDrill({ question, clicked });
-  if (!drill) {
-    return [];
-  }
-
-  const { query, dimensions, breakoutOptions } = drill;
-
-  return [
-    {
-      name: "pivot-by-category",
-      section: "breakout",
-      buttonType: "token",
-      title: clicked ? (
-        t`Category`
-      ) : (
-        <span>
-          {jt`Break out by ${(
-            <span className="text-dark">{t`category`}</span>
-          )}`}
-        </span>
-      ),
-      popover: function PivotDrillPopover({ onChangeCardAndRun, onClose }) {
-        return (
-          <BreakoutPopover
-            query={query}
-            breakoutOptions={breakoutOptions}
-            onChangeBreakout={breakout => {
-              const nextCard = question.pivot([breakout], dimensions).card();
-              onChangeCardAndRun({ nextCard });
-            }}
-            onClose={onClose}
-            alwaysExpanded
-          />
-        );
-      },
-    },
-  ];
-};
diff --git a/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx b/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx
new file mode 100644
index 00000000000..7b6749c4b2e
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/PivotByCategoryDrill.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { t, jt } from "ttag";
+
+import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
+
+import type { Card } from "metabase-types/api";
+import { pivotByCategoryDrill } from "metabase-lib/queries/drills/pivot-drill";
+
+import type { ClickActionPopoverProps, Drill } from "../../types";
+
+const PivotByCategoryDrill: Drill = ({ question, clicked }) => {
+  const drill = pivotByCategoryDrill({ question, clicked });
+  if (!drill) {
+    return [];
+  }
+
+  const { query, dimensions, breakoutOptions } = drill;
+
+  const PivotDrillPopover = ({
+    onChangeCardAndRun,
+    onClose,
+  }: ClickActionPopoverProps) => {
+    return (
+      <BreakoutPopover
+        query={query}
+        breakoutOptions={breakoutOptions}
+        onChangeBreakout={breakout => {
+          const nextCard = question.pivot([breakout], dimensions).card();
+
+          // Casting deprecated `metabase-types/Card` to `metabase-types/api/Card`
+          onChangeCardAndRun({ nextCard: nextCard as Card });
+        }}
+        onClose={onClose}
+        alwaysExpanded
+      />
+    );
+  };
+
+  return [
+    {
+      name: "pivot-by-category",
+      title: clicked ? (
+        t`Category`
+      ) : (
+        <span>
+          {jt`Break out by ${(
+            <span className="text-dark">{t`category`}</span>
+          )}`}
+        </span>
+      ),
+      section: "breakout",
+      buttonType: "token",
+      popover: PivotDrillPopover,
+    },
+  ];
+};
+
+export default PivotByCategoryDrill;
diff --git a/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx b/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx
deleted file mode 100644
index d75feb7f669..00000000000
--- a/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t, jt } from "ttag";
-import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
-import { pivotByLocationDrill } from "metabase-lib/queries/drills/pivot-drill";
-
-export default ({ question, clicked }) => {
-  const drill = pivotByLocationDrill({ question, clicked });
-  if (!drill) {
-    return [];
-  }
-
-  const { query, dimensions, breakoutOptions } = drill;
-
-  return [
-    {
-      name: "pivot-by-location",
-      section: "breakout",
-      buttonType: "token",
-      title: clicked ? (
-        t`Location`
-      ) : (
-        <span>
-          {jt`Break out by ${(
-            <span className="text-dark">{t`location`}</span>
-          )}`}
-        </span>
-      ),
-      popover: function PivotDrillPopover({ onChangeCardAndRun, onClose }) {
-        return (
-          <BreakoutPopover
-            query={query}
-            breakoutOptions={breakoutOptions}
-            onChangeBreakout={breakout => {
-              const nextCard = question.pivot([breakout], dimensions).card();
-              onChangeCardAndRun({ nextCard });
-            }}
-            onClose={onClose}
-            alwaysExpanded
-          />
-        );
-      },
-    },
-  ];
-};
diff --git a/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx b/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx
new file mode 100644
index 00000000000..815c88d5eaa
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/PivotByLocationDrill.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { t, jt } from "ttag";
+
+import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
+
+import type { Card } from "metabase-types/api";
+import { pivotByLocationDrill } from "metabase-lib/queries/drills/pivot-drill";
+
+import type { ClickActionPopoverProps, Drill } from "../../types";
+
+const PivotByLocationDrill: Drill = ({ question, clicked }) => {
+  const drill = pivotByLocationDrill({ question, clicked });
+  if (!drill) {
+    return [];
+  }
+
+  const { query, dimensions, breakoutOptions } = drill;
+
+  const PivotDrillPopover = ({
+    onChangeCardAndRun,
+    onClose,
+  }: ClickActionPopoverProps) => {
+    return (
+      <BreakoutPopover
+        query={query}
+        breakoutOptions={breakoutOptions}
+        onChangeBreakout={breakout => {
+          const nextCard = question.pivot([breakout], dimensions).card();
+
+          // Casting deprecated `metabase-types/Card` to `metabase-types/api/Card`
+          onChangeCardAndRun({ nextCard: nextCard as Card });
+        }}
+        onClose={onClose}
+        alwaysExpanded
+      />
+    );
+  };
+
+  return [
+    {
+      name: "pivot-by-location",
+      title: clicked ? (
+        t`Location`
+      ) : (
+        <span>
+          {jt`Break out by ${(
+            <span className="text-dark">{t`location`}</span>
+          )}`}
+        </span>
+      ),
+      section: "breakout",
+      buttonType: "token",
+      popover: PivotDrillPopover,
+    },
+  ];
+};
+
+export default PivotByLocationDrill;
diff --git a/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx b/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx
deleted file mode 100644
index 33ca646c00f..00000000000
--- a/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-/* eslint-disable react/prop-types */
-import React from "react";
-import { t, jt } from "ttag";
-import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
-import { pivotByTimeDrill } from "metabase-lib/queries/drills/pivot-drill";
-
-export default ({ question, clicked }) => {
-  const drill = pivotByTimeDrill({ question, clicked });
-  if (!drill) {
-    return [];
-  }
-
-  const { query, dimensions, breakoutOptions } = drill;
-
-  return [
-    {
-      name: "pivot-by-time",
-      section: "breakout",
-      buttonType: "token",
-      title: clicked ? (
-        t`Time`
-      ) : (
-        <span>
-          {jt`Break out by ${(<span className="text-dark">{t`time`}</span>)}`}
-        </span>
-      ),
-      popover: function PivotDrillPopover({ onChangeCardAndRun, onClose }) {
-        return (
-          <BreakoutPopover
-            query={query}
-            breakoutOptions={breakoutOptions}
-            onChangeBreakout={breakout => {
-              const nextCard = question.pivot([breakout], dimensions).card();
-              onChangeCardAndRun({ nextCard });
-            }}
-            onClose={onClose}
-            alwaysExpanded
-          />
-        );
-      },
-    },
-  ];
-};
diff --git a/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx b/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx
new file mode 100644
index 00000000000..c4ce6723f6d
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/PivotByTimeDrill.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import { t, jt } from "ttag";
+
+import BreakoutPopover from "metabase/query_builder/components/BreakoutPopover";
+
+import type { Card } from "metabase-types/api";
+import { pivotByTimeDrill } from "metabase-lib/queries/drills/pivot-drill";
+
+import type { ClickActionPopoverProps, Drill } from "../../types";
+
+const PivotByTimeDrill: Drill = ({ question, clicked }) => {
+  const drill = pivotByTimeDrill({ question, clicked });
+  if (!drill) {
+    return [];
+  }
+
+  const { query, dimensions, breakoutOptions } = drill;
+
+  const PivotDrillPopover = ({
+    onChangeCardAndRun,
+    onClose,
+  }: ClickActionPopoverProps) => (
+    <BreakoutPopover
+      query={query}
+      breakoutOptions={breakoutOptions}
+      onChangeBreakout={breakout => {
+        const nextCard = question.pivot([breakout], dimensions).card();
+
+        // Casting deprecated `metabase-types/Card` to `metabase-types/api/card`
+        onChangeCardAndRun({ nextCard: nextCard as Card });
+      }}
+      onClose={onClose}
+      alwaysExpanded
+    />
+  );
+
+  return [
+    {
+      name: "pivot-by-time",
+      title: clicked ? (
+        t`Time`
+      ) : (
+        <span>
+          {jt`Break out by ${(<span className="text-dark">{t`time`}</span>)}`}
+        </span>
+      ),
+      section: "breakout",
+      buttonType: "token",
+      popover: PivotDrillPopover,
+    },
+  ];
+};
+
+export default PivotByTimeDrill;
diff --git a/frontend/src/metabase/modes/components/drill/QuickFilterDrill.jsx b/frontend/src/metabase/modes/components/drill/QuickFilterDrill.tsx
similarity index 69%
rename from frontend/src/metabase/modes/components/drill/QuickFilterDrill.jsx
rename to frontend/src/metabase/modes/components/drill/QuickFilterDrill.tsx
index 6e368900d26..1f4053c17f4 100644
--- a/frontend/src/metabase/modes/components/drill/QuickFilterDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/QuickFilterDrill.tsx
@@ -1,23 +1,23 @@
-/* eslint-disable react/prop-types */
 import React from "react";
 import {
   quickFilterDrill,
   quickFilterDrillQuestion,
 } from "metabase-lib/queries/drills/quick-filter-drill";
+import type { Drill } from "../../types";
 
-export default function QuickFilterDrill({ question, clicked }) {
+const QuickFilterDrill: Drill = ({ question, clicked }) => {
   const drill = quickFilterDrill({ question, clicked });
   if (!drill) {
     return [];
   }
 
-  const { operators } = drill;
-
-  return operators.map(({ name, operator }) => ({
+  return drill.operators.map(({ name, operator }) => ({
     name: operator,
+    title: <span className="h2">{name}</span>,
     section: "filter",
     buttonType: "token-filter",
-    title: <span className="h2">{name}</span>,
     question: () => quickFilterDrillQuestion({ question, clicked, operator }),
   }));
-}
+};
+
+export default QuickFilterDrill;
diff --git a/frontend/src/metabase/modes/components/drill/SortAction.jsx b/frontend/src/metabase/modes/components/drill/SortDrill.ts
similarity index 78%
rename from frontend/src/metabase/modes/components/drill/SortAction.jsx
rename to frontend/src/metabase/modes/components/drill/SortDrill.ts
index ecb174eeeeb..d25cd8afef8 100644
--- a/frontend/src/metabase/modes/components/drill/SortAction.jsx
+++ b/frontend/src/metabase/modes/components/drill/SortDrill.ts
@@ -3,25 +3,26 @@ import {
   sortDrill,
   sortDrillQuestion,
 } from "metabase-lib/queries/drills/sort-drill";
+import type { ClickActionBase, Drill } from "../../types";
 
-const ACTIONS = {
+const ACTIONS: Record<string, ClickActionBase> = {
   asc: {
     name: "sort-ascending",
+    icon: "arrow_up",
     section: "sort",
     buttonType: "sort",
-    icon: "arrow_up",
     tooltip: t`Sort ascending`,
   },
   desc: {
     name: "sort-descending",
+    icon: "arrow_down",
     section: "sort",
     buttonType: "sort",
-    icon: "arrow_down",
     tooltip: t`Sort descending`,
   },
 };
 
-export default ({ question, clicked }) => {
+const SortDrill: Drill = ({ question, clicked }) => {
   const drill = sortDrill({ question, clicked });
   if (!drill) {
     return [];
@@ -33,3 +34,5 @@ export default ({ question, clicked }) => {
     question: () => sortDrillQuestion({ question, clicked, sortDirection }),
   }));
 };
+
+export default SortDrill;
diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js b/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.ts
similarity index 75%
rename from frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js
rename to frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.ts
index 097a99ab765..e41a2d9998c 100644
--- a/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.js
+++ b/frontend/src/metabase/modes/components/drill/SummarizeColumnByTimeDrill.ts
@@ -3,8 +3,9 @@ import {
   summarizeColumnByTimeDrill,
   summarizeColumnByTimeDrillQuestion,
 } from "metabase-lib/queries/drills/summarize-column-by-time-drill";
+import type { Drill } from "../../types";
 
-export default ({ question, clicked = {} }) => {
+const SummarizeColumnByTimeDrill: Drill = ({ question, clicked = {} }) => {
   if (!summarizeColumnByTimeDrill({ question, clicked })) {
     return [];
   }
@@ -12,11 +13,13 @@ export default ({ question, clicked = {} }) => {
   return [
     {
       name: "summarize-by-time",
-      buttonType: "horizontal",
+      title: t`Sum over time`,
       section: "summarize",
       icon: "line",
-      title: t`Sum over time`,
+      buttonType: "horizontal",
       question: () => summarizeColumnByTimeDrillQuestion({ question, clicked }),
     },
   ];
 };
+
+export default SummarizeColumnByTimeDrill;
diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js
deleted file mode 100644
index 3fc2a687c34..00000000000
--- a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { t } from "ttag";
-import {
-  summarizeColumnDrill,
-  summarizeColumnDrillQuestion,
-} from "metabase-lib/queries/drills/summarize-column-drill";
-
-const ACTIONS = {
-  sum: {
-    title: t`Sum`,
-    section: "sum",
-    buttonType: "token",
-  },
-  avg: {
-    title: t`Avg`,
-    section: "sum",
-    buttonType: "token",
-  },
-  distinct: {
-    title: t`Distinct values`,
-    section: "sum",
-    buttonType: "token",
-  },
-};
-
-export default ({ question, clicked = {} }) => {
-  const drill = summarizeColumnDrill({ question, clicked });
-  if (!drill) {
-    return [];
-  }
-
-  const { aggregationOperators } = drill;
-
-  return aggregationOperators.map(aggregationOperator => ({
-    ...ACTIONS[aggregationOperator.short],
-    name: aggregationOperator.short,
-    question: () =>
-      summarizeColumnDrillQuestion({ question, clicked, aggregationOperator }),
-    action: () => dispatch =>
-      // HACK: drill through closes sidebars, so open sidebar asynchronously
-      setTimeout(() => dispatch({ type: "metabase/qb/EDIT_SUMMARY" })),
-  }));
-};
diff --git a/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts
new file mode 100644
index 00000000000..fe4531c623a
--- /dev/null
+++ b/frontend/src/metabase/modes/components/drill/SummarizeColumnDrill.ts
@@ -0,0 +1,71 @@
+import { t } from "ttag";
+
+import {
+  summarizeColumnDrill,
+  summarizeColumnDrillQuestion,
+} from "metabase-lib/queries/drills/summarize-column-drill";
+
+import type {
+  ClickAction,
+  ClickActionBase,
+  Drill,
+  DrillOptions,
+} from "../../types";
+
+type AggregationOperator = {
+  short: string;
+};
+
+const ACTIONS: Record<string, Omit<ClickActionBase, "name">> = {
+  sum: {
+    title: t`Sum`,
+    section: "sum",
+    buttonType: "token",
+  },
+  avg: {
+    title: t`Avg`,
+    section: "sum",
+    buttonType: "token",
+  },
+  distinct: {
+    title: t`Distinct values`,
+    section: "sum",
+    buttonType: "token",
+  },
+};
+
+function getAction(
+  operator: AggregationOperator,
+  { question, clicked }: DrillOptions,
+): ClickAction {
+  return {
+    ...ACTIONS[operator.short],
+    name: operator.short,
+    question: () =>
+      summarizeColumnDrillQuestion({
+        question,
+        clicked,
+        aggregationOperator: operator,
+      }),
+    action: () => dispatch =>
+      // HACK: drill through closes sidebars, so open sidebar asynchronously
+      setTimeout(() => dispatch({ type: "metabase/qb/EDIT_SUMMARY" })),
+  };
+}
+
+const SummarizeColumnDrill: Drill = (opts: DrillOptions) => {
+  const { question, clicked } = opts;
+
+  const drill = summarizeColumnDrill({ question, clicked });
+  if (!drill) {
+    return [];
+  }
+
+  const { aggregationOperators } = drill;
+
+  return aggregationOperators
+    .filter(operator => operator)
+    .map(operator => getAction(operator as AggregationOperator, opts));
+};
+
+export default SummarizeColumnDrill;
diff --git a/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.jsx b/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.ts
similarity index 84%
rename from frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.jsx
rename to frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.ts
index 3ecb08c0e07..851a2a0aa85 100644
--- a/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/UnderlyingRecordsDrill.ts
@@ -4,8 +4,9 @@ import {
   underlyingRecordsDrill,
   underlyingRecordsDrillQuestion,
 } from "metabase-lib/queries/drills/underlying-records-drill";
+import type { Drill } from "../../types";
 
-export default ({ question, clicked }) => {
+export const UnderlyingRecordsDrill: Drill = ({ question, clicked }) => {
   const drill = underlyingRecordsDrill({ question, clicked });
   if (!drill) {
     return [];
@@ -26,11 +27,13 @@ export default ({ question, clicked }) => {
   return [
     {
       name: "underlying-records",
+      title: actionTitle,
       section: "records",
-      buttonType: "horizontal",
       icon: "table_spaced",
-      title: actionTitle,
+      buttonType: "horizontal",
       question: () => underlyingRecordsDrillQuestion({ question, clicked }),
     },
   ];
 };
+
+export default UnderlyingRecordsDrill;
diff --git a/frontend/src/metabase/modes/components/drill/ZoomDrill.jsx b/frontend/src/metabase/modes/components/drill/ZoomDrill.ts
similarity index 77%
rename from frontend/src/metabase/modes/components/drill/ZoomDrill.jsx
rename to frontend/src/metabase/modes/components/drill/ZoomDrill.ts
index f524641c6c6..251e81a86a5 100644
--- a/frontend/src/metabase/modes/components/drill/ZoomDrill.jsx
+++ b/frontend/src/metabase/modes/components/drill/ZoomDrill.ts
@@ -3,8 +3,9 @@ import {
   zoomDrill,
   zoomDrillQuestion,
 } from "metabase-lib/queries/drills/zoom-drill";
+import type { Drill } from "../../types";
 
-export default ({ question, clicked }) => {
+const ZoomDrill: Drill = ({ question, clicked }) => {
   if (!zoomDrill({ question, clicked })) {
     return [];
   }
@@ -12,11 +13,13 @@ export default ({ question, clicked }) => {
   return [
     {
       name: "timeseries-zoom",
-      section: "zoom",
       title: t`Zoom in`,
-      buttonType: "horizontal",
+      section: "zoom",
       icon: "zoom_in",
+      buttonType: "horizontal",
       question: () => zoomDrillQuestion({ question, clicked }),
     },
   ];
 };
+
+export default ZoomDrill;
diff --git a/frontend/src/metabase/modes/components/modes/DefaultMode.jsx b/frontend/src/metabase/modes/components/modes/DefaultMode.jsx
index 991b03ad837..5a3a2328206 100644
--- a/frontend/src/metabase/modes/components/modes/DefaultMode.jsx
+++ b/frontend/src/metabase/modes/components/modes/DefaultMode.jsx
@@ -1,4 +1,4 @@
-import SortAction from "../drill/SortAction";
+import SortDrill from "../drill/SortDrill";
 import ObjectDetailDrill from "../drill/ObjectDetailDrill";
 import QuickFilterDrill from "../drill/QuickFilterDrill";
 import ForeignKeyDrill from "../drill/ForeignKeyDrill";
@@ -7,14 +7,14 @@ import UnderlyingRecordsDrill from "../drill/UnderlyingRecordsDrill";
 import AutomaticDashboardDrill from "../drill/AutomaticDashboardDrill";
 import CompareToRestDrill from "../drill/CompareToRestDrill";
 import ZoomDrill from "../drill/ZoomDrill";
-import FormatAction from "../drill/FormatAction";
+import FormatDrill from "../drill/FormatDrill";
 import DashboardClickDrill from "../drill/DashboardClickDrill";
 
 const DefaultMode = {
   name: "default",
   drills: [
     ZoomDrill,
-    SortAction,
+    SortDrill,
     ObjectDetailDrill,
     QuickFilterDrill,
     ForeignKeyDrill,
@@ -22,7 +22,7 @@ const DefaultMode = {
     UnderlyingRecordsDrill,
     AutomaticDashboardDrill,
     CompareToRestDrill,
-    FormatAction,
+    FormatDrill,
     DashboardClickDrill,
   ],
 };
diff --git a/frontend/src/metabase/modes/types.ts b/frontend/src/metabase/modes/types.ts
new file mode 100644
index 00000000000..f624c2aa66a
--- /dev/null
+++ b/frontend/src/metabase/modes/types.ts
@@ -0,0 +1,99 @@
+import React from "react";
+import type {
+  DatasetColumn,
+  RowValue,
+  Series,
+  VisualizationSettings,
+} from "metabase-types/api";
+import type { Dispatch, ReduxAction } from "metabase-types/store";
+import type { OnChangeCardAndRun } from "metabase/visualizations/types";
+import type Question from "metabase-lib/Question";
+
+type DimensionValue = {
+  value: RowValue;
+  column: DatasetColumn;
+};
+
+export type ClickObject = {
+  value?: RowValue;
+  column?: DatasetColumn;
+  dimensions?: DimensionValue[];
+  settings?: Record<string, unknown>;
+  extraData?: Record<string, unknown>;
+  seriesIndex?: number;
+  origin?: {
+    row: RowValue;
+    cols: DatasetColumn[];
+  };
+  event?: MouseEvent;
+  element?: HTMLElement;
+};
+
+type Dispatcher = (dispatch: Dispatch) => void;
+
+export type ClickActionPopoverProps = {
+  series: Series;
+  onChangeCardAndRun: OnChangeCardAndRun;
+  onChange: (settings: VisualizationSettings) => void;
+  onResize: (...args: unknown[]) => void;
+  onClose: () => void;
+};
+
+export type ClickActionButtonType =
+  | "formatting"
+  | "horizontal"
+  | "info"
+  | "token"
+  | "token-filter"
+  | "sort";
+
+export type ClickActionBase = {
+  name: string;
+  title?: React.ReactNode;
+  section: string;
+  icon?: string;
+  buttonType: ClickActionButtonType;
+  default?: boolean;
+  tooltip?: string;
+  extra?: () => Record<string, unknown>;
+};
+
+type ReduxClickAction = ClickActionBase & {
+  action: () => ReduxAction | Dispatcher;
+};
+
+type QuestionChangeClickAction = ClickActionBase & {
+  question: () => Question;
+};
+
+type PopoverClickAction = ClickActionBase & {
+  popoverProps?: Record<string, unknown>;
+  popover: (props: ClickActionPopoverProps) => JSX.Element;
+};
+
+type UrlClickAction = ClickActionBase & {
+  ignoreSiteUrl?: boolean;
+  url: () => string;
+};
+
+type RegularClickAction =
+  | ReduxClickAction
+  | QuestionChangeClickAction
+  | PopoverClickAction
+  | UrlClickAction;
+
+type AlwaysDefaultClickAction = Omit<
+  RegularClickAction,
+  "title" | "section" | "default" | "buttonType" | "tooltip"
+> & {
+  defaultAlways: true;
+};
+
+export type ClickAction = RegularClickAction | AlwaysDefaultClickAction;
+
+export type DrillOptions = {
+  question: Question;
+  clicked?: ClickObject;
+};
+
+export type Drill = (options: DrillOptions) => ClickAction[];
diff --git a/frontend/src/metabase/query_builder/components/BreakoutPopover.jsx b/frontend/src/metabase/query_builder/components/BreakoutPopover.tsx
similarity index 59%
rename from frontend/src/metabase/query_builder/components/BreakoutPopover.jsx
rename to frontend/src/metabase/query_builder/components/BreakoutPopover.tsx
index 79afac4de53..90893b399a3 100644
--- a/frontend/src/metabase/query_builder/components/BreakoutPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/BreakoutPopover.tsx
@@ -1,18 +1,35 @@
-/* eslint-disable react/prop-types */
 import React from "react";
+
+import type Field from "metabase-lib/metadata/Field";
+import type Breakout from "metabase-lib/queries/structured/Breakout";
+import type DimensionOptions from "metabase-lib/DimensionOptions";
+import type StructuredQuery from "metabase-lib/queries/StructuredQuery";
+
 import { BreakoutFieldList } from "./BreakoutPopover.styled";
 
+interface BreakoutPopoverProps {
+  className?: string;
+  query: StructuredQuery;
+  breakout?: Breakout;
+  breakoutOptions: DimensionOptions;
+  width?: number;
+  maxHeight?: number;
+  alwaysExpanded?: boolean;
+  onChangeBreakout: (breakout: Field) => void;
+  onClose: () => void;
+}
+
 const BreakoutPopover = ({
   className,
+  query,
   breakout,
   onChangeBreakout,
-  query,
   breakoutOptions,
   onClose,
   maxHeight,
   alwaysExpanded,
   width = 400,
-}) => {
+}: BreakoutPopoverProps) => {
   const table = query.table();
   // FieldList requires table
   if (!table) {
@@ -29,7 +46,7 @@ const BreakoutPopover = ({
       query={query}
       metadata={query.metadata()}
       fieldOptions={fieldOptions}
-      onFieldChange={field => {
+      onFieldChange={(field: Field) => {
         onChangeBreakout(field);
         if (onClose) {
           onClose();
diff --git a/frontend/src/metabase/visualizations/types.ts b/frontend/src/metabase/visualizations/types.ts
new file mode 100644
index 00000000000..7b20a7bd55a
--- /dev/null
+++ b/frontend/src/metabase/visualizations/types.ts
@@ -0,0 +1,8 @@
+import type { Card } from "metabase-types/api";
+
+type OnChangeCardAndRunOpts = {
+  previousCard?: Card;
+  nextCard: Card;
+};
+
+export type OnChangeCardAndRun = (opts: OnChangeCardAndRunOpts) => void;
-- 
GitLab