diff --git a/frontend/src/metabase/lib/formatting/types.ts b/frontend/src/metabase/lib/formatting/types.ts
index 21414dfbe0b3cb4400822d4e6e2b674758913fcf..57b69026373d7ff4484c209c26059e670392882f 100644
--- a/frontend/src/metabase/lib/formatting/types.ts
+++ b/frontend/src/metabase/lib/formatting/types.ts
@@ -9,6 +9,7 @@ export interface OptionsType extends TimeOnlyOptions {
   click_behavior?: any;
   clicked?: any;
   column?: any;
+  column_title?: string;
   compact?: boolean;
   date_abbreviate?: boolean;
   date_format?: string;
@@ -30,6 +31,7 @@ export interface OptionsType extends TimeOnlyOptions {
   removeDay?: boolean;
   removeYear?: boolean;
   rich?: boolean;
+  show_mini_bar?: boolean;
   suffix?: string;
   type?: string;
   view_as?: string | null;
diff --git a/frontend/src/metabase/visualizations/components/TableSimple/TableCell.jsx b/frontend/src/metabase/visualizations/components/TableSimple/TableCell.tsx
similarity index 72%
rename from frontend/src/metabase/visualizations/components/TableSimple/TableCell.jsx
rename to frontend/src/metabase/visualizations/components/TableSimple/TableCell.tsx
index 8fc199429f88c5bbd6b83c6f84a08838747b37a8..7d7d5c9fa4bcb282d94c7c8c04baa1792f8c2748 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple/TableCell.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple/TableCell.tsx
@@ -1,9 +1,9 @@
-/* eslint-disable react/prop-types */
-import { useCallback, useMemo } from "react";
+import { useCallback, useMemo, isValidElement } from "react";
 import cx from "classnames";
 
 import ExternalLink from "metabase/core/components/ExternalLink";
 
+import type { OptionsType } from "metabase/lib/formatting/types";
 import { formatValue } from "metabase/lib/formatting";
 import {
   getTableCellClickedObject,
@@ -11,11 +11,31 @@ import {
   isColumnRightAligned,
 } from "metabase/visualizations/lib/table";
 import { getColumnExtent } from "metabase/visualizations/lib/utils";
+
+import type {
+  DatasetColumn,
+  DatasetData,
+  RowValue,
+  RowValues,
+  Series,
+  VisualizationSettings,
+} from "metabase-types/api";
+import type { ClickObject } from "metabase-lib";
 import { isID, isFK } from "metabase-lib/types/utils/isa";
 
 import MiniBar from "../MiniBar";
 import { CellRoot, CellContent } from "./TableCell.styled";
 
+type GetCellDataOpts = {
+  value: RowValue;
+  clicked: ClickObject;
+  extraData: Record<string, unknown>;
+  cols: DatasetColumn[];
+  rows: RowValues[];
+  columnIndex: number;
+  columnSettings: OptionsType;
+};
+
 function getCellData({
   value,
   clicked,
@@ -24,7 +44,7 @@ function getCellData({
   rows,
   columnIndex,
   columnSettings,
-}) {
+}: GetCellDataOpts) {
   if (value == null) {
     return "-";
   }
@@ -46,7 +66,25 @@ function getCellData({
   });
 }
 
-function TableCell({
+interface TableCellProps {
+  value: RowValue;
+  data: DatasetData;
+  series: Series;
+  settings: VisualizationSettings;
+  rowIndex: number;
+  columnIndex: number;
+  isPivoted: boolean;
+  getCellBackgroundColor: (
+    value: RowValue,
+    rowIndex: number,
+    columnName: string,
+  ) => string | undefined;
+  getExtraDataForClick: (clickObject: ClickObject) => Record<string, unknown>;
+  checkIsVisualizationClickable: (clickObject: ClickObject) => boolean;
+  onVisualizationClick?: (clickObject: ClickObject) => void;
+}
+
+export function TableCell({
   value,
   data,
   series,
@@ -58,7 +96,7 @@ function TableCell({
   getExtraDataForClick,
   checkIsVisualizationClickable,
   onVisualizationClick,
-}) {
+}: TableCellProps) {
   const { rows, cols } = data;
   const column = cols[columnIndex];
   const columnSettings = settings.column(column);
@@ -66,6 +104,7 @@ function TableCell({
   const clickedRowData = useMemo(
     () =>
       getTableClickedObjectRowData(
+        // @ts-expect-error -- visualizations/lib/table should be typed
         series,
         rowIndex,
         columnIndex,
@@ -107,13 +146,13 @@ function TableCell({
     [value, clicked, extraData, cols, rows, columnIndex, columnSettings],
   );
 
-  const isLink = cellData && cellData.type === ExternalLink;
+  const isLink = isValidElement(cellData) && cellData.type === ExternalLink;
   const isClickable = !isLink;
 
   const onClick = useCallback(
     e => {
       if (checkIsVisualizationClickable(clicked)) {
-        onVisualizationClick({
+        onVisualizationClick?.({
           ...clicked,
           element: e.currentTarget,
           extraData,
@@ -155,5 +194,3 @@ function TableCell({
     </CellRoot>
   );
 }
-
-export default TableCell;
diff --git a/frontend/src/metabase/visualizations/components/TableSimple/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple/TableSimple.tsx
similarity index 83%
rename from frontend/src/metabase/visualizations/components/TableSimple/TableSimple.jsx
rename to frontend/src/metabase/visualizations/components/TableSimple/TableSimple.tsx
index 55eb9c1aa35fd2a1247a42bc575d61341cfe9b4f..e70b4be91ee334cd88d97e543e198772fb63a1ad 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple/TableSimple.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple/TableSimple.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/prop-types */
 import { useCallback, useLayoutEffect, useMemo, useState, useRef } from "react";
 import { getIn } from "icepick";
 import _ from "underscore";
@@ -8,9 +7,19 @@ import { Ellipsified } from "metabase/core/components/Ellipsified";
 
 import { isPositiveInteger } from "metabase/lib/number";
 import { isColumnRightAligned } from "metabase/visualizations/lib/table";
+
+import type {
+  Card,
+  DatasetColumn,
+  DatasetData,
+  RowValue,
+  Series,
+  VisualizationSettings,
+} from "metabase-types/api";
+import type { ClickObject } from "metabase-lib";
 import { isID } from "metabase-lib/types/utils/isa";
 
-import TableCell from "./TableCell";
+import { TableCell } from "./TableCell";
 import TableFooter from "./TableFooter";
 import {
   Root,
@@ -21,11 +30,13 @@ import {
   SortIcon,
 } from "./TableSimple.styled";
 
-function getBoundingClientRectSafe(ref) {
+function getBoundingClientRectSafe(ref: {
+  current?: HTMLElement | null;
+}): Partial<DOMRect> {
   return ref.current?.getBoundingClientRect?.() ?? {};
 }
 
-function formatCellValueForSorting(value, column) {
+function formatCellValueForSorting(value: RowValue, column: DatasetColumn) {
   if (typeof value === "string") {
     if (isID(column) && isPositiveInteger(value)) {
       return parseInt(value, 10);
@@ -39,7 +50,23 @@ function formatCellValueForSorting(value, column) {
   return value;
 }
 
-function TableSimple({
+interface TableSimpleProps {
+  card: Card;
+  data: DatasetData;
+  series: Series;
+  settings: VisualizationSettings;
+  height: number;
+  isDashboard?: boolean;
+  isEditing?: boolean;
+  isPivoted: boolean;
+  className?: string;
+  getColumnTitle: (colIndex: number) => string;
+  getExtraDataForClick: (clickObject: ClickObject) => Record<string, unknown>;
+  onVisualizationClick?: (clickObject: ClickObject) => void;
+  visualizationIsClickable?: (clickObject: ClickObject) => boolean;
+}
+
+function TableSimpleInner({
   card,
   data,
   series,
@@ -51,7 +78,7 @@ function TableSimple({
   visualizationIsClickable,
   getColumnTitle,
   getExtraDataForClick,
-}) {
+}: TableSimpleProps) {
   const [page, setPage] = useState(0);
   const [pageSize, setPageSize] = useState(1);
   const [sortColumn, setSortColumn] = useState(null);
@@ -62,7 +89,7 @@ function TableSimple({
   const firstRowRef = useRef(null);
 
   useLayoutEffect(() => {
-    const { height: headerHeight } = getBoundingClientRectSafe(headerRef);
+    const { height: headerHeight = 0 } = getBoundingClientRectSafe(headerRef);
     const { height: footerHeight = 0 } = getBoundingClientRectSafe(footerRef);
     const { height: rowHeight = 0 } = getBoundingClientRectSafe(firstRowRef);
     const currentPageSize = Math.floor(
@@ -87,10 +114,10 @@ function TableSimple({
 
   const checkIsVisualizationClickable = useCallback(
     clickedItem => {
-      return (
+      return Boolean(
         onVisualizationClick &&
-        visualizationIsClickable &&
-        visualizationIsClickable(clickedItem)
+          visualizationIsClickable &&
+          visualizationIsClickable(clickedItem),
       );
     },
     [onVisualizationClick, visualizationIsClickable],
@@ -217,7 +244,7 @@ function TableSimple({
   );
 }
 
-export default ExplicitSize({
+export const TableSimple = ExplicitSize<TableSimpleProps>({
   refreshMode: props =>
     props.isDashboard && !props.isEditing ? "debounceLeading" : "throttle",
-})(TableSimple);
+})(TableSimpleInner);
diff --git a/frontend/src/metabase/visualizations/components/TableSimple/index.ts b/frontend/src/metabase/visualizations/components/TableSimple/index.ts
index 600fdb7f87c649f85afcab3d9d97de365d580c07..4f4c724ab2533a1b84ac72b071f451eddcb7994f 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple/index.ts
+++ b/frontend/src/metabase/visualizations/components/TableSimple/index.ts
@@ -1,2 +1 @@
-// eslint-disable-next-line import/no-default-export -- deprecated usage
-export { default } from "./TableSimple";
+export * from "./TableSimple";
diff --git a/frontend/src/metabase/visualizations/types/visualization.ts b/frontend/src/metabase/visualizations/types/visualization.ts
index 0ea73629cbd4f80b6bf40624ed57aa1f646a3491..3af55d834eee2231ee927aa08c3015d2e7d9af09 100644
--- a/frontend/src/metabase/visualizations/types/visualization.ts
+++ b/frontend/src/metabase/visualizations/types/visualization.ts
@@ -1,5 +1,6 @@
 import type {
   Card,
+  DatasetColumn,
   DatasetData,
   RawSeries,
   Series,
@@ -10,6 +11,8 @@ import type { ClickObject } from "metabase/visualizations/types";
 import type { ColorGetter } from "metabase/static-viz/lib/colors";
 import type { OptionsType } from "metabase/lib/formatting/types";
 import type { IconName, IconProps } from "metabase/ui";
+
+import type Metadata from "metabase-lib/metadata/Metadata";
 import type Query from "metabase-lib/queries/Query";
 
 import type { HoveredObject } from "./hover";
@@ -46,6 +49,7 @@ export interface VisualizationProps {
   series: Series;
   card: Card;
   data: DatasetData;
+  metadata: Metadata;
   rawSeries: RawSeries;
   settings: ComputedVisualizationSettings;
   headerIcon: IconProps;
@@ -88,6 +92,24 @@ export interface VisualizationProps {
   onUpdateWarnings?: any;
 }
 
+export type ColumnSettingDefinition<TValue, TProps = unknown> = {
+  title?: string;
+  hint?: string;
+  widget?: string | React.ComponentType<any>;
+  default?: TValue;
+  props?: TProps;
+  inline?: boolean;
+  readDependencies?: string[];
+  getDefault?: (col: DatasetColumn) => TValue;
+  getHidden?: (col: DatasetColumn, settings: OptionsType) => boolean;
+  getProps?: (
+    col: DatasetColumn,
+    settings: OptionsType,
+    onChange: (value: TValue) => void,
+    extra: { series: Series },
+  ) => TProps;
+};
+
 export type VisualizationSettingDefinition<TValue, TProps = void> = {
   section?: string;
   title?: string;
diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.tsx
similarity index 82%
rename from frontend/src/metabase/visualizations/visualizations/Table.jsx
rename to frontend/src/metabase/visualizations/visualizations/Table.tsx
index f352fcfebb8f77adee7cab12850c5ba6d7708904..a9f386c6302612f5b53808a70047163f006d3489 100644
--- a/frontend/src/metabase/visualizations/visualizations/Table.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Table.tsx
@@ -1,12 +1,10 @@
-/* eslint-disable react/prop-types */
 import { Component } from "react";
-
 import { t } from "ttag";
 import _ from "underscore";
 import cx from "classnames";
-import * as DataGrid from "metabase/lib/data_grid";
-import { getOptionFromColumn } from "metabase/visualizations/lib/settings/utils";
+
 import { formatColumn } from "metabase/lib/formatting";
+import * as DataGrid from "metabase/lib/data_grid";
 
 import ChartSettingLinkUrlInput from "metabase/visualizations/components/settings/ChartSettingLinkUrlInput";
 import ChartSettingsTableFormatting, {
@@ -20,12 +18,19 @@ import {
   getTitleForColumn,
   isPivoted as _isPivoted,
 } from "metabase/visualizations/lib/settings/column";
-
+import { getOptionFromColumn } from "metabase/visualizations/lib/settings/utils";
+import { getDefaultPivotColumn } from "metabase/visualizations/lib/utils";
 import {
   getDefaultSize,
   getMinSize,
 } from "metabase/visualizations/shared/utils/sizes";
-import { getDefaultPivotColumn } from "metabase/visualizations/lib/utils";
+
+import type {
+  DatasetColumn,
+  DatasetData,
+  Series,
+  VisualizationSettings,
+} from "metabase-types/api";
 import * as Lib from "metabase-lib";
 import Question from "metabase-lib/Question";
 import {
@@ -40,10 +45,20 @@ import {
 import { findColumnIndexForColumnSetting } from "metabase-lib/queries/utils/dataset";
 import * as Q_DEPRECATED from "metabase-lib/queries/utils";
 
-import TableSimple from "../components/TableSimple";
+import type { ColumnSettingDefinition, VisualizationProps } from "../types";
+import { TableSimple } from "../components/TableSimple";
 import TableInteractive from "../components/TableInteractive/TableInteractive.jsx";
 
-export default class Table extends Component {
+interface TableProps extends VisualizationProps {
+  isShowingDetailsOnlyColumns?: boolean;
+}
+
+interface TableState {
+  data: Pick<DatasetData, "cols" | "rows" | "results_timezone"> | null;
+  question: Question | null;
+}
+
+class Table extends Component<TableProps, TableState> {
   static uiName = t`Table`;
   static identifier = "table";
   static iconName = "table";
@@ -52,19 +67,15 @@ export default class Table extends Component {
   static minSize = getMinSize("table");
   static defaultSize = getDefaultSize("table");
 
-  static isSensible({ cols, rows }) {
+  static isSensible() {
     return true;
   }
 
-  static isLiveResizable(series) {
+  static isLiveResizable() {
     return false;
   }
 
-  static checkRenderable([
-    {
-      data: { cols, rows },
-    },
-  ]) {
+  static checkRenderable() {
     // scalar can always be rendered, nothing needed here
   }
 
@@ -77,8 +88,8 @@ export default class Table extends Component {
       title: t`Pivot table`,
       widget: "toggle",
       inline: true,
-      getHidden: ([{ card, data }]) => data && data.cols.length !== 3,
-      getDefault: ([{ card, data }]) => {
+      getHidden: ([{ data }]: Series) => data && data.cols.length !== 3,
+      getDefault: ([{ card, data }]: Series) => {
         if (
           !data ||
           data.cols.length !== 3 ||
@@ -100,20 +111,18 @@ export default class Table extends Component {
         {
           data: { cols, rows },
         },
-      ]) => {
+      ]: Series) => {
         return getDefaultPivotColumn(cols, rows)?.name;
       },
-      getProps: (
-        [
-          {
-            data: { cols },
-          },
-        ],
-        settings,
-      ) => ({
+      getProps: ([
+        {
+          data: { cols },
+        },
+      ]: Series) => ({
         options: cols.filter(isDimension).map(getOptionFromColumn),
       }),
-      getHidden: (series, settings) => !settings["table.pivot"],
+      getHidden: (series: Series, settings: VisualizationSettings) =>
+        !settings["table.pivot"],
       readDependencies: ["table.pivot"],
       persistDefault: true,
     },
@@ -121,7 +130,10 @@ export default class Table extends Component {
       section: t`Columns`,
       title: t`Cell column`,
       widget: "field",
-      getDefault: ([{ data }], { "table.pivot_column": pivotCol }) => {
+      getDefault: (
+        [{ data }]: Series,
+        { "table.pivot_column": pivotCol }: VisualizationSettings,
+      ) => {
         // We try to show numeric values in pivot cells, but if none are
         // available, we fall back to the last column in the unpivoted table
         const nonPivotCols = data.cols.filter(c => c.name !== pivotCol);
@@ -129,24 +141,15 @@ export default class Table extends Component {
         const { name } = nonPivotCols.find(isMetric) || lastCol || {};
         return name;
       },
-      getProps: (
-        [
-          {
-            data: { cols },
-          },
-        ],
-        settings,
-      ) => ({
+      getProps: ([
+        {
+          data: { cols },
+        },
+      ]: Series) => ({
         options: cols.map(getOptionFromColumn),
       }),
-      getHidden: (
-        [
-          {
-            data: { cols },
-          },
-        ],
-        settings,
-      ) => !settings["table.pivot"],
+      getHidden: (series: Series, settings: VisualizationSettings) =>
+        !settings["table.pivot"],
       readDependencies: ["table.pivot", "table.pivot_column"],
       persistDefault: true,
     },
@@ -156,19 +159,16 @@ export default class Table extends Component {
       section: t`Conditional Formatting`,
       widget: ChartSettingsTableFormatting,
       default: [],
-      getProps: (series, settings) => ({
+      getProps: (series: Series, settings: VisualizationSettings) => ({
         cols: series[0].data.cols.filter(isFormattable),
         isPivoted: settings["table.pivot"],
       }),
 
-      getHidden: (
-        [
-          {
-            data: { cols },
-          },
-        ],
-        settings,
-      ) => cols.filter(isFormattable).length === 0,
+      getHidden: ([
+        {
+          data: { cols },
+        },
+      ]: Series) => cols.filter(isFormattable).length === 0,
       readDependencies: ["table.pivot"],
     },
     "table._cell_background_getter": {
@@ -177,8 +177,8 @@ export default class Table extends Component {
           {
             data: { rows, cols },
           },
-        ],
-        settings,
+        ]: Series,
+        settings: VisualizationSettings,
       ) {
         return makeCellBackgroundGetter(
           rows,
@@ -191,8 +191,11 @@ export default class Table extends Component {
     },
   };
 
-  static columnSettings = column => {
-    const settings = {
+  static columnSettings = (column: DatasetColumn) => {
+    const settings: Record<
+      string,
+      ColumnSettingDefinition<unknown, unknown>
+    > = {
       column_title: {
         title: t`Column title`,
         widget: "input",
@@ -200,6 +203,7 @@ export default class Table extends Component {
       },
       click_behavior: {},
     };
+
     if (isNumber(column)) {
       settings["show_mini_bar"] = {
         title: t`Show a mini bar chart`,
@@ -250,7 +254,7 @@ export default class Table extends Component {
         settings["view_as"] !== "link" && settings["view_as"] !== "email_link",
       readDependencies: ["view_as"],
       getProps: (
-        col,
+        column,
         settings,
         onChange,
         {
@@ -276,7 +280,7 @@ export default class Table extends Component {
       getHidden: (_, settings) => settings["view_as"] !== "link",
       readDependencies: ["view_as"],
       getProps: (
-        col,
+        column,
         settings,
         onChange,
         {
@@ -297,19 +301,16 @@ export default class Table extends Component {
     return settings;
   };
 
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      data: null,
-    };
-  }
+  state: TableState = {
+    data: null,
+    question: null,
+  };
 
   UNSAFE_componentWillMount() {
     this._updateData(this.props);
   }
 
-  UNSAFE_componentWillReceiveProps(newProps) {
+  UNSAFE_componentWillReceiveProps(newProps: VisualizationProps) {
     if (
       newProps.series !== this.props.series ||
       !_.isEqual(newProps.settings, this.props.settings)
@@ -318,7 +319,7 @@ export default class Table extends Component {
     }
   }
 
-  _updateData({ series, settings, metadata }) {
+  _updateData({ series, settings, metadata }: VisualizationProps) {
     const [{ card, data }] = series;
 
     if (Table.isPivoted(series, settings)) {
@@ -335,18 +336,12 @@ export default class Table extends Component {
         (col, index) => index !== pivotIndex && index !== cellIndex,
       );
       this.setState({
-        data: DataGrid.pivot(
-          data,
-          normalIndex,
-          pivotIndex,
-          cellIndex,
-          settings,
-        ),
+        data: DataGrid.pivot(data, normalIndex, pivotIndex, cellIndex),
       });
     } else {
       const { cols, rows, results_timezone } = data;
       const columnSettings = settings["table.columns"];
-      const columnIndexes = columnSettings
+      const columnIndexes = (columnSettings || [])
         .filter(
           columnSetting =>
             columnSetting.enabled || this.props.isShowingDetailsOnlyColumns,
@@ -362,6 +357,7 @@ export default class Table extends Component {
           rows: rows.map(row => columnIndexes.map(i => row[i])),
           results_timezone,
         },
+
         // construct a Question that is in-sync with query results
         // cache it here for performance reasons
         question: new Question(card, metadata),
@@ -371,7 +367,7 @@ export default class Table extends Component {
 
   // shared helpers for table implementations
 
-  getColumnTitle = columnIndex => {
+  getColumnTitle = (columnIndex: number) => {
     const cols = this.state.data && this.state.data.cols;
     if (!cols) {
       return null;
@@ -380,7 +376,7 @@ export default class Table extends Component {
     return getTitleForColumn(cols[columnIndex], series, settings);
   };
 
-  getColumnSortDirection = columnIndex => {
+  getColumnSortDirection = (columnIndex: number) => {
     const { question, data } = this.state;
     if (!question || !data) {
       return;
@@ -410,7 +406,7 @@ export default class Table extends Component {
     const { series, isDashboard, settings } = this.props;
     const { data } = this.state;
     const isPivoted = Table.isPivoted(series, settings);
-    const areAllColumnsHidden = data.cols.length === 0;
+    const areAllColumnsHidden = data?.cols.length === 0;
     const TableComponent = isDashboard ? TableSimple : TableInteractive;
 
     if (!data) {
@@ -450,3 +446,6 @@ export default class Table extends Component {
     );
   }
 }
+
+// eslint-disable-next-line import/no-default-export
+export default Table;