diff --git a/frontend/src/metabase-lib/metadata/Field.ts b/frontend/src/metabase-lib/metadata/Field.ts
index 56a4b5d7e2407223c26b081d4b5c41d9780b6ae7..a3ad8ff30156cb3f5a0f9d63ffe2a2568c213ead 100644
--- a/frontend/src/metabase-lib/metadata/Field.ts
+++ b/frontend/src/metabase-lib/metadata/Field.ts
@@ -511,7 +511,7 @@ class FieldInner extends Base {
       }));
   };
 
-  clone(fieldMetadata) {
+  clone(fieldMetadata?: FieldMetadata) {
     if (fieldMetadata instanceof Field) {
       throw new Error("`fieldMetadata` arg must be a plain object");
     }
diff --git a/frontend/src/metabase-types/api/field.ts b/frontend/src/metabase-types/api/field.ts
index 97010da81716b8b0e90c06c8c05df0c0081ff2f2..b81fee3d4bf881c8fb061dade3fefa376d31d76f 100644
--- a/frontend/src/metabase-types/api/field.ts
+++ b/frontend/src/metabase-types/api/field.ts
@@ -103,6 +103,7 @@ export interface Field {
   max_value?: number;
   min_value?: number;
   has_field_values: FieldValuesType;
+  has_more_values?: boolean;
 
   caveats?: string | null;
   points_of_interest?: string;
diff --git a/frontend/src/metabase-types/api/mocks/field.ts b/frontend/src/metabase-types/api/mocks/field.ts
index 2c4becdccc78eaf32b84a087219df81260256153..6b6ee4de4056170e5f619b5a2020f4829ba5d6eb 100644
--- a/frontend/src/metabase-types/api/mocks/field.ts
+++ b/frontend/src/metabase-types/api/mocks/field.ts
@@ -32,6 +32,7 @@ export const createMockField = (opts?: Partial<Field>): Field => ({
   fingerprint: null,
 
   has_field_values: "list",
+  has_more_values: false,
 
   last_analyzed: new Date().toISOString(),
   created_at: new Date().toISOString(),
diff --git a/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.jsx b/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx
similarity index 67%
rename from frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.jsx
rename to frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx
index efbae67125dbd6ef9e2f199e8736786052a6df83..9ff35242a97d17ca6fe1f59dd4dba0e2c0135094 100644
--- a/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.jsx
+++ b/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.tsx
@@ -1,9 +1,6 @@
-/* eslint-disable react/prop-types */
-import { useState, useRef } from "react";
-
+import { useState, useRef, StyleHTMLAttributes } from "react";
 import { useMount, useUnmount } from "react-use";
 
-import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { jt, t } from "ttag";
 import _ from "underscore";
@@ -21,6 +18,7 @@ import AutoExpanding from "metabase/hoc/AutoExpanding";
 
 import { addRemappings } from "metabase/redux/metadata";
 import { defer } from "metabase/lib/promise";
+import type { LayoutRendererArgs } from "metabase/components/TokenField/TokenField";
 import {
   fetchCardParameterValues,
   fetchDashboardParameterValues,
@@ -28,6 +26,25 @@ import {
 } from "metabase/parameters/actions";
 
 import Fields from "metabase/entities/fields";
+import type { State } from "metabase-types/store";
+
+import type {
+  CardId,
+  Dashboard,
+  DashboardId,
+  Parameter,
+  FieldId,
+  FieldReference,
+  FieldValue,
+  RowValue,
+  Field as APIField,
+  ParameterValues,
+} from "metabase-types/api";
+
+import type Field from "metabase-lib/metadata/Field";
+import type Question from "metabase-lib/Question";
+
+import type { ValuesMode, LoadingStateType } from "./types";
 
 import {
   canUseParameterEndpoints,
@@ -49,11 +66,6 @@ import { OptionsMessage, StyledEllipsified } from "./FieldValuesWidget.styled";
 
 const MAX_SEARCH_RESULTS = 100;
 
-const fieldValuesWidgetPropTypes = {
-  addRemappings: PropTypes.func,
-  expand: PropTypes.bool,
-};
-
 const mapDispatchToProps = {
   addRemappings,
   fetchFieldValues: Fields.objectActions.fetchFieldValues,
@@ -62,7 +74,7 @@ const mapDispatchToProps = {
   fetchDashboardParameterValues,
 };
 
-function mapStateToProps(state, { fields = [] }) {
+function mapStateToProps(state: State, { fields = [] }: { fields: Field[] }) {
   return {
     fields: fields.map(
       field =>
@@ -71,7 +83,74 @@ function mapStateToProps(state, { fields = [] }) {
   };
 }
 
-function FieldValuesWidgetInner({
+type FieldValuesResponse = {
+  payload: APIField;
+};
+
+interface FetcherOptions {
+  query?: string;
+  parameter?: Parameter;
+  parameters?: Parameter[];
+  dashboardId?: DashboardId;
+  cardId?: CardId;
+}
+
+export interface IFieldValuesWidgetProps {
+  color?: string;
+  maxResults?: number;
+  style?: StyleHTMLAttributes<HTMLDivElement>;
+  formatOptions?: Record<string, any>;
+  maxWidth?: number;
+  minWidth?: number;
+
+  expand?: boolean;
+  disableList?: boolean;
+  disableSearch?: boolean;
+  disablePKRemappingForSearch?: boolean;
+  alwaysShowOptions?: boolean;
+  showOptionsInPopover?: boolean;
+
+  fetchFieldValues: ({
+    id,
+  }: {
+    id: FieldId | FieldReference;
+  }) => Promise<FieldValuesResponse>;
+  fetchParameterValues: (options: FetcherOptions) => Promise<ParameterValues>;
+  fetchCardParameterValues: (
+    options: FetcherOptions,
+  ) => Promise<ParameterValues>;
+  fetchDashboardParameterValues: (
+    options: FetcherOptions,
+  ) => Promise<ParameterValues>;
+
+  addRemappings: (
+    value: FieldReference | FieldId,
+    options: FieldValue[],
+  ) => void;
+
+  parameter?: Parameter;
+  parameters?: Parameter[];
+  fields: Field[];
+  dashboard?: Dashboard;
+  question?: Question;
+
+  value: string[];
+  onChange: (value: string[]) => void;
+
+  multi?: boolean;
+  autoFocus?: boolean;
+  className?: string;
+  prefix?: string;
+  placeholder?: string;
+  forceTokenField?: boolean;
+  checkedColor?: string;
+
+  valueRenderer?: (value: string | number) => JSX.Element;
+  optionRenderer?: (option: FieldValue) => JSX.Element;
+  layoutRenderer?: (props: LayoutRendererArgs) => JSX.Element;
+}
+
+export function FieldValuesWidgetInner({
   color = "purple",
   maxResults = MAX_SEARCH_RESULTS,
   alwaysShowOptions = true,
@@ -106,11 +185,11 @@ function FieldValuesWidgetInner({
   valueRenderer,
   optionRenderer,
   layoutRenderer,
-}) {
-  const [options, setOptions] = useState([]);
-  const [loadingState, setLoadingState] = useState("INIT");
-  const [lastValue, setLastValue] = useState("");
-  const [valuesMode, setValuesMode] = useState(
+}: IFieldValuesWidgetProps) {
+  const [options, setOptions] = useState<FieldValue[]>([]);
+  const [loadingState, setLoadingState] = useState<LoadingStateType>("INIT");
+  const [lastValue, setLastValue] = useState<string>("");
+  const [valuesMode, setValuesMode] = useState<ValuesMode>(
     getValuesMode({
       parameter,
       fields,
@@ -125,19 +204,17 @@ function FieldValuesWidgetInner({
     }
   });
 
-  const _cancel = useRef(null);
+  const _cancel = useRef<null | (() => void)>(null);
 
   useUnmount(() => {
-    if (_cancel.current) {
-      _cancel.current();
-    }
+    _cancel?.current?.();
   });
 
-  const fetchValues = async query => {
+  const fetchValues = async (query?: string) => {
     setLoadingState("LOADING");
     setOptions([]);
 
-    let newOptions = [];
+    let newOptions: FieldValue[] = [];
     let newValuesMode = valuesMode;
     try {
       if (canUseDashboardEndpoints(dashboard)) {
@@ -175,7 +252,7 @@ function FieldValuesWidgetInner({
     }
   };
 
-  const fetchFieldValues = async query => {
+  const fetchFieldValues = async (query?: string): Promise<FieldValue[]> => {
     if (query == null) {
       const nonVirtualFields = getNonVirtualFields(fields);
 
@@ -185,7 +262,7 @@ function FieldValuesWidgetInner({
 
       // extract the field values from the API response(s)
       // the entity loader has inconsistent return structure, so we have to handle both
-      const fieldValues = nonVirtualFields.map(
+      const fieldValues: FieldValue[][] = nonVirtualFields.map(
         (field, index) =>
           results[index]?.payload?.values ??
           Fields.selectors.getFieldValues(results[index]?.payload, {
@@ -196,7 +273,7 @@ function FieldValuesWidgetInner({
       return dedupeValues(fieldValues);
     } else {
       const cancelDeferred = defer();
-      const cancelled = cancelDeferred.promise;
+      const cancelled: Promise<unknown> = cancelDeferred.promise;
       _cancel.current = () => {
         _cancel.current = null;
         cancelDeferred.resolve();
@@ -217,22 +294,22 @@ function FieldValuesWidgetInner({
     }
   };
 
-  const fetchParameterValues = async query => {
+  const fetchParameterValues = async (query?: string) => {
     return fetchParameterValuesProp({
       parameter,
       query,
     });
   };
 
-  const fetchCardParameterValues = async query => {
+  const fetchCardParameterValues = async (query?: string) => {
     return fetchCardParameterValuesProp({
-      cardId: question.id(),
+      cardId: question?.id(),
       parameter,
       query,
     });
   };
 
-  const fetchDashboardParameterValues = async query => {
+  const fetchDashboardParameterValues = async (query?: string) => {
     return fetchDashboardParameterValuesProp({
       dashboardId: dashboard?.id,
       parameter,
@@ -241,7 +318,8 @@ function FieldValuesWidgetInner({
     });
   };
 
-  const updateRemappings = options => {
+  // ? this may rely on field mutations
+  const updateRemappings = (options: FieldValue[]) => {
     if (showRemapping(fields)) {
       const [field] = fields;
       if (
@@ -252,7 +330,7 @@ function FieldValuesWidgetInner({
     }
   };
 
-  const onInputChange = value => {
+  const onInputChange = (value: string) => {
     let localValuesMode = valuesMode;
 
     // override "search" mode when searching is unnecessary
@@ -273,7 +351,7 @@ function FieldValuesWidgetInner({
   };
 
   const search = useRef(
-    _.debounce(async value => {
+    _.debounce(async (value: string) => {
       if (!value) {
         setLoadingState("LOADED");
         return;
@@ -285,7 +363,7 @@ function FieldValuesWidgetInner({
     }, 500),
   );
 
-  const _search = value => {
+  const _search = (value: string) => {
     if (_cancel.current) {
       _cancel.current();
     }
@@ -295,26 +373,33 @@ function FieldValuesWidgetInner({
   };
 
   if (!valueRenderer) {
-    valueRenderer = value =>
-      renderValue(fields, formatOptions, value, {
+    valueRenderer = (value: string | number) =>
+      renderValue({
+        fields,
+        formatOptions,
+        value,
         autoLoad: true,
         compact: false,
       });
   }
 
   if (!optionRenderer) {
-    optionRenderer = option =>
-      renderValue(fields, formatOptions, option[0], {
-        autoLoad: false,
-      });
+    optionRenderer = (option: FieldValue) =>
+      renderValue({ fields, formatOptions, value: option[0], autoLoad: false });
   }
 
   if (!layoutRenderer) {
     layoutRenderer = showOptionsInPopover
       ? undefined
-      : layoutProps => (
+      : ({
+          optionsList,
+          isFocused,
+          isAllSelected,
+          isFiltered,
+          valuesList,
+        }: LayoutRendererArgs) => (
           <div>
-            {layoutProps.valuesList}
+            {valuesList}
             {renderOptions({
               alwaysShowOptions,
               parameter,
@@ -324,7 +409,10 @@ function FieldValuesWidgetInner({
               loadingState,
               options,
               valuesMode,
-              ...layoutProps,
+              optionsList,
+              isFocused,
+              isAllSelected,
+              isFiltered,
             })}
           </div>
         );
@@ -336,7 +424,6 @@ function FieldValuesWidgetInner({
     disableSearch,
     placeholder,
     disablePKRemappingForSearch,
-    loadingState,
     options,
     valuesMode,
   });
@@ -354,7 +441,7 @@ function FieldValuesWidgetInner({
     options,
   });
 
-  const parseFreeformValue = value => {
+  const parseFreeformValue = (value: string | number) => {
     return isNumeric(fields[0], parameter)
       ? parseNumberValue(value)
       : parseStringValue(value);
@@ -362,8 +449,9 @@ function FieldValuesWidgetInner({
 
   return (
     <div
+      data-testid="field-values-widget"
       style={{
-        width: expand ? maxWidth : null,
+        width: expand ? maxWidth : undefined,
         minWidth: minWidth,
         maxWidth: maxWidth,
       }}
@@ -372,9 +460,9 @@ function FieldValuesWidgetInner({
         <LoadingState />
       ) : isListMode && hasListValues && multi ? (
         <ListField
-          isDashboardFilter={parameter}
+          isDashboardFilter={!!parameter}
           placeholder={tokenFieldPlaceholder}
-          value={value.filter(v => v != null)}
+          value={value?.filter((v: string) => v != null)}
           onChange={onChange}
           options={options}
           optionRenderer={optionRenderer}
@@ -382,7 +470,7 @@ function FieldValuesWidgetInner({
         />
       ) : isListMode && hasListValues && !multi ? (
         <SingleSelectListField
-          isDashboardFilter={parameter}
+          isDashboardFilter={!!parameter}
           placeholder={tokenFieldPlaceholder}
           value={value.filter(v => v != null)}
           onChange={onChange}
@@ -430,22 +518,22 @@ function FieldValuesWidgetInner({
 
 export const FieldValuesWidget = AutoExpanding(FieldValuesWidgetInner);
 
-FieldValuesWidget.propTypes = fieldValuesWidgetPropTypes;
-
 const LoadingState = () => (
   <div className="flex layout-centered align-center" style={{ minHeight: 82 }}>
     <LoadingSpinner size={32} />
   </div>
 );
 
-const NoMatchState = ({ fields }) => {
-  if (fields.length === 1) {
+const NoMatchState = ({ fields }: { fields: (Field | null)[] }) => {
+  if (fields.length === 1 && !!fields[0]) {
     const [{ display_name }] = fields;
 
     return (
       <OptionsMessage>
         {jt`No matching ${(
-          <StyledEllipsified>{display_name}</StyledEllipsified>
+          <StyledEllipsified key={display_name}>
+            {display_name}
+          </StyledEllipsified>
         )} found.`}
       </OptionsMessage>
     );
@@ -458,8 +546,24 @@ const EveryOptionState = () => (
   <OptionsMessage>{t`Including every option in your filter probably won’t do much…`}</OptionsMessage>
 );
 
+// eslint-disable-next-line import/no-default-export
 export default connect(mapStateToProps, mapDispatchToProps)(FieldValuesWidget);
 
+interface RenderOptionsProps {
+  alwaysShowOptions: boolean;
+  parameter?: Parameter;
+  fields: Field[];
+  disableSearch: boolean;
+  disablePKRemappingForSearch?: boolean;
+  loadingState: LoadingStateType;
+  options: FieldValue[];
+  valuesMode: ValuesMode;
+  optionsList: React.ReactNode;
+  isFocused: boolean;
+  isAllSelected: boolean;
+  isFiltered: boolean;
+}
+
 function renderOptions({
   alwaysShowOptions,
   parameter,
@@ -473,7 +577,7 @@ function renderOptions({
   isFocused,
   isAllSelected,
   isFiltered,
-}) {
+}: RenderOptionsProps) {
   if (alwaysShowOptions || isFocused) {
     if (optionsList) {
       return optionsList;
@@ -503,8 +607,9 @@ function renderOptions({
       } else if (loadingState === "LOADED" && isFiltered) {
         return (
           <NoMatchState
-            fields={fields.map(field =>
-              field.searchField(disablePKRemappingForSearch),
+            fields={fields.map(
+              field =>
+                field.searchField(disablePKRemappingForSearch) as Field | null,
             )}
           />
         );
@@ -513,7 +618,19 @@ function renderOptions({
   }
 }
 
-function renderValue(fields, formatOptions, value, options) {
+function renderValue({
+  fields,
+  formatOptions,
+  value,
+  autoLoad,
+  compact,
+}: {
+  fields: Field[];
+  formatOptions: Record<string, any>;
+  value: RowValue;
+  autoLoad?: boolean;
+  compact?: boolean;
+}) {
   return (
     <ValueComponent
       value={value}
@@ -521,7 +638,8 @@ function renderValue(fields, formatOptions, value, options) {
       maximumFractionDigits={20}
       remap={showRemapping(fields)}
       {...formatOptions}
-      {...options}
+      autoLoad={autoLoad}
+      compact={compact}
     />
   );
 }
diff --git a/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.unit.spec.js b/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.unit.spec.tsx
similarity index 85%
rename from frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.unit.spec.js
rename to frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.unit.spec.tsx
index 617197cb41456423fb0d52c314456ddc956c0e88..bfd57fc27d0e687c796be842adc07e1510e97132 100644
--- a/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.unit.spec.js
+++ b/frontend/src/metabase/components/FieldValuesWidget/FieldValuesWidget.unit.spec.tsx
@@ -1,5 +1,3 @@
-import "mutationobserver-shim";
-
 import userEvent from "@testing-library/user-event";
 import {
   getBrokenUpTextMatcher,
@@ -10,7 +8,10 @@ import {
 import { setupFieldSearchValuesEndpoints } from "__support__/server-mocks";
 
 import { checkNotNull } from "metabase/core/utils/types";
-import { FieldValuesWidget } from "metabase/components/FieldValuesWidget";
+import {
+  FieldValuesWidget,
+  IFieldValuesWidgetProps,
+} from "metabase/components/FieldValuesWidget";
 
 import {
   ORDERS,
@@ -19,6 +20,7 @@ import {
   PRODUCT_CATEGORY_VALUES,
   PEOPLE_SOURCE_VALUES,
 } from "metabase-types/api/mocks/presets";
+import Field from "metabase-lib/metadata/Field";
 
 import {
   state,
@@ -30,22 +32,37 @@ import {
   metadataWithSearchValuesField,
 } from "./testMocks";
 
-async function setup({ fields, values, searchValue, ...props }) {
+async function setup({
+  fields,
+  prefix,
+  searchValue,
+  ...props
+}: {
+  fields: (Field | null | undefined)[];
+  searchValue?: string;
+  prefix?: string;
+} & Omit<Partial<IFieldValuesWidgetProps>, "fields">) {
   const fetchFieldValues = jest.fn(({ id }) => ({
-    payload: fields.find(f => f.id === id),
+    payload: fields.filter(checkNotNull).find(f => f?.id === id),
   }));
 
-  fields.forEach(field => {
-    setupFieldSearchValuesEndpoints(field.id, searchValue);
-  });
+  if (searchValue) {
+    fields.forEach(field => {
+      setupFieldSearchValuesEndpoints(field?.id as number, searchValue);
+    });
+  }
 
   renderWithProviders(
     <FieldValuesWidget
       value={[]}
-      fields={fields}
+      fields={fields.filter(checkNotNull)}
       onChange={jest.fn()}
-      fetchFieldValues={fetchFieldValues}
+      fetchFieldValues={fetchFieldValues as any}
+      fetchParameterValues={jest.fn()}
+      fetchDashboardParameterValues={jest.fn()}
+      fetchCardParameterValues={jest.fn()}
       addRemappings={jest.fn()}
+      prefix={prefix}
       {...props}
     />,
     {
@@ -150,9 +167,12 @@ describe("FieldValuesWidget", () => {
 
     describe("has_field_values = search", () => {
       it("should have 'Search by Category or enter an ID' as the placeholder text", async () => {
-        const field = metadata.field(SEARCHABLE_FK_FIELD_ID).clone();
+        const field = metadata.field(SEARCHABLE_FK_FIELD_ID)?.clone();
         const remappedField = metadata.field(PRODUCTS.CATEGORY);
-        field.remappedField = () => remappedField;
+
+        if (field) {
+          field.remappedField = () => remappedField;
+        }
 
         await setup({ fields: [field] });
 
@@ -174,11 +194,13 @@ describe("FieldValuesWidget", () => {
 
   describe("multiple fields", () => {
     it("list multiple fields together", async () => {
-      const categoryField = metadata.field(PRODUCTS.CATEGORY).clone();
-      categoryField.values = PRODUCT_CATEGORY_VALUES.values;
+      const categoryField = metadata.field(PRODUCTS.CATEGORY)?.clone();
+      const sourceField = metadata.field(PEOPLE.SOURCE)?.clone();
 
-      const sourceField = metadata.field(PEOPLE.SOURCE).clone();
-      sourceField.values = PEOPLE_SOURCE_VALUES.values;
+      if (categoryField && sourceField) {
+        categoryField.values = PRODUCT_CATEGORY_VALUES.values;
+        sourceField.values = PEOPLE_SOURCE_VALUES.values;
+      }
 
       await setup({ fields: [categoryField, sourceField] });
 
@@ -232,7 +254,9 @@ describe("FieldValuesWidget", () => {
 
   describe("custom expressions", () => {
     const valuesField = checkNotNull(metadata.field(LISTABLE_PK_FIELD_ID));
-    const expressionField = checkNotNull(metadata.field(EXPRESSION_FIELD_ID));
+    const expressionField = checkNotNull(
+      metadata.field(EXPRESSION_FIELD_ID as any),
+    );
 
     it("should not call fetchFieldValues", async () => {
       const { fetchFieldValues } = await setup({
@@ -252,7 +276,7 @@ describe("FieldValuesWidget", () => {
   describe("NoMatchState", () => {
     it("should display field title when one field passed and there are no matching results", async () => {
       const field = metadataWithSearchValuesField.field(PEOPLE.PASSWORD);
-      const displayName = field.display_name; // "Password"
+      const displayName = field?.display_name; // "Password"
       const searchValue = "somerandomvalue";
 
       await setup({
diff --git a/frontend/src/metabase/components/FieldValuesWidget/index.jsx b/frontend/src/metabase/components/FieldValuesWidget/index.jsx
deleted file mode 100644
index 58e8c6d0f9d001b5b215ea9a4dfd5dfa27548dbf..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/components/FieldValuesWidget/index.jsx
+++ /dev/null
@@ -1 +0,0 @@
-export { default, FieldValuesWidget } from "./FieldValuesWidget";
diff --git a/frontend/src/metabase/components/FieldValuesWidget/index.ts b/frontend/src/metabase/components/FieldValuesWidget/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7737d6d05aa97baa6bf19982ffc89542e26bf9c2
--- /dev/null
+++ b/frontend/src/metabase/components/FieldValuesWidget/index.ts
@@ -0,0 +1,3 @@
+// eslint-disable-next-line import/no-default-export -- deprecated usage
+export { default } from "./FieldValuesWidget";
+export * from "./FieldValuesWidget";
diff --git a/frontend/src/metabase/components/FieldValuesWidget/testMocks.js b/frontend/src/metabase/components/FieldValuesWidget/testMocks.ts
similarity index 98%
rename from frontend/src/metabase/components/FieldValuesWidget/testMocks.js
rename to frontend/src/metabase/components/FieldValuesWidget/testMocks.ts
index bf5e4589d7d3593d5f422c0b533d5e656715e230..3678ea2cfe3943f5f94b465e238c2d94cdb67da4 100644
--- a/frontend/src/metabase/components/FieldValuesWidget/testMocks.js
+++ b/frontend/src/metabase/components/FieldValuesWidget/testMocks.ts
@@ -69,7 +69,7 @@ const database = createSampleDatabase({
           has_more_values: true,
         }),
         createMockField({
-          id: EXPRESSION_FIELD_ID,
+          id: EXPRESSION_FIELD_ID as any,
           field_ref: ["expression", "CC"],
         }),
       ],
diff --git a/frontend/src/metabase/components/FieldValuesWidget/types.ts b/frontend/src/metabase/components/FieldValuesWidget/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3fe496aaeaeb8698fa4236b218c5ec0efd1fbf6
--- /dev/null
+++ b/frontend/src/metabase/components/FieldValuesWidget/types.ts
@@ -0,0 +1,2 @@
+export type ValuesMode = "search" | "list" | "none";
+export type LoadingStateType = "LOADING" | "LOADED" | "INIT";
diff --git a/frontend/src/metabase/components/FieldValuesWidget/utils.js b/frontend/src/metabase/components/FieldValuesWidget/utils.ts
similarity index 66%
rename from frontend/src/metabase/components/FieldValuesWidget/utils.js
rename to frontend/src/metabase/components/FieldValuesWidget/utils.ts
index a0fbb7abd58146318198f6f838ca7004b614bbef..cf0c9f2669eb1e63b08803643593294603c22a7d 100644
--- a/frontend/src/metabase/components/FieldValuesWidget/utils.js
+++ b/frontend/src/metabase/components/FieldValuesWidget/utils.ts
@@ -4,6 +4,9 @@ import _ from "underscore";
 import { MetabaseApi } from "metabase/services";
 import { stripId } from "metabase/lib/formatting";
 
+import type { Dashboard, Parameter, FieldValue } from "metabase-types/api";
+import type Field from "metabase-lib/metadata/Field";
+
 import {
   isIdParameter,
   isNumberParameter,
@@ -16,19 +19,32 @@ import {
   canSearchParameterValues,
   getSourceType,
 } from "metabase-lib/parameters/utils/parameter-source";
+import Question from "metabase-lib/Question";
+
+import type { ValuesMode } from "./types";
 
 export async function searchFieldValues(
-  { fields, value, disablePKRemappingForSearch, maxResults },
-  cancelled,
+  {
+    fields,
+    value,
+    disablePKRemappingForSearch,
+    maxResults,
+  }: {
+    fields: Field[];
+    value: string;
+    disablePKRemappingForSearch?: boolean;
+    maxResults: number;
+  },
+  cancelled: Promise<unknown>,
 ) {
-  let options = dedupeValues(
+  let options: null | FieldValue[] = dedupeValues(
     await Promise.all(
-      fields.map(field =>
+      fields.map((field: Field) =>
         MetabaseApi.field_search(
           {
             value,
             fieldId: field.id,
-            searchFieldId: field.searchField(disablePKRemappingForSearch).id,
+            searchFieldId: field.searchField(disablePKRemappingForSearch)?.id,
             limit: maxResults,
           },
           { cancelled },
@@ -37,36 +53,44 @@ export async function searchFieldValues(
     ),
   );
 
-  options = options.map(result => [].concat(result));
+  options = options?.map(result => (Array.isArray(result) ? result : [result]));
   return options;
 }
 
-export function getNonVirtualFields(fields) {
+export function getNonVirtualFields(fields: Field[]) {
   return fields.filter(field => !field.isVirtual());
 }
 
-export function dedupeValues(valuesList) {
+export function dedupeValues(valuesList: FieldValue[][]): FieldValue[] {
   const uniqueValueMap = new Map(valuesList.flat().map(o => [o[0], o]));
   return Array.from(uniqueValueMap.values());
 }
 
-export function canUseParameterEndpoints(parameter) {
+export function canUseParameterEndpoints(parameter?: Parameter) {
   return parameter != null;
 }
 
-export function canUseCardEndpoints(question) {
+export function canUseCardEndpoints(question?: Question) {
   return question?.isSaved();
 }
 
-export function canUseDashboardEndpoints(dashboard) {
+export function canUseDashboardEndpoints(dashboard?: Dashboard) {
   return dashboard?.id;
 }
 
-export function showRemapping(fields) {
+export function showRemapping(fields: Field[]) {
   return fields.length === 1;
 }
 
-export function shouldList({ parameter, fields, disableSearch }) {
+export function shouldList({
+  parameter,
+  fields,
+  disableSearch,
+}: {
+  parameter?: Parameter;
+  fields: Field[];
+  disableSearch: boolean;
+}) {
   if (disableSearch) {
     return false;
   } else {
@@ -76,7 +100,10 @@ export function shouldList({ parameter, fields, disableSearch }) {
   }
 }
 
-function getNonSearchableTokenFieldPlaceholder(firstField, parameter) {
+function getNonSearchableTokenFieldPlaceholder(
+  firstField: Field,
+  parameter?: Parameter,
+) {
   if (parameter) {
     if (isIdParameter(parameter)) {
       return t`Enter an ID`;
@@ -105,21 +132,26 @@ function getNonSearchableTokenFieldPlaceholder(firstField, parameter) {
   return t`Enter some text`;
 }
 
-export function searchField(field, disablePKRemappingForSearch) {
+export function searchField(
+  field: Field,
+  disablePKRemappingForSearch: boolean,
+) {
   return field.searchField(disablePKRemappingForSearch);
 }
 
 function getSearchableTokenFieldPlaceholder(
-  parameter,
-  fields,
-  firstField,
-  disablePKRemappingForSearch,
+  parameter: Parameter | undefined,
+  fields: Field[],
+  firstField: Field,
+  disablePKRemappingForSearch?: boolean,
 ) {
   let placeholder;
 
   const names = new Set(
-    fields.map(field =>
-      stripId(field.searchField(disablePKRemappingForSearch).display_name),
+    fields.map((field: Field) =>
+      stripId(
+        field?.searchField?.(disablePKRemappingForSearch)?.display_name ?? "",
+      ),
     ),
   );
 
@@ -143,7 +175,17 @@ function getSearchableTokenFieldPlaceholder(
   return placeholder;
 }
 
-export function hasList({ parameter, fields, disableSearch, options }) {
+export function hasList({
+  parameter,
+  fields,
+  disableSearch,
+  options,
+}: {
+  parameter?: Parameter;
+  fields: Field[];
+  disableSearch: boolean;
+  options: FieldValue[];
+}) {
   return (
     shouldList({ parameter, fields, disableSearch }) && !_.isEmpty(options)
   );
@@ -153,10 +195,10 @@ export function hasList({ parameter, fields, disableSearch, options }) {
 // wasn't truncated, then we don't need to do another search because TypeaheadListing
 // will filter the previous result client-side
 export function isExtensionOfPreviousSearch(
-  value,
-  lastValue,
-  options,
-  maxResults,
+  value: string,
+  lastValue: string,
+  options: FieldValue[],
+  maxResults: number,
 ) {
   return (
     lastValue &&
@@ -171,6 +213,12 @@ export function isSearchable({
   disableSearch,
   disablePKRemappingForSearch,
   valuesMode,
+}: {
+  parameter?: Parameter;
+  fields: Field[];
+  disableSearch: boolean;
+  disablePKRemappingForSearch?: boolean;
+  valuesMode?: ValuesMode;
 }) {
   if (disableSearch) {
     return false;
@@ -191,6 +239,14 @@ export function getTokenFieldPlaceholder({
   disablePKRemappingForSearch,
   options,
   valuesMode,
+}: {
+  fields: Field[];
+  parameter?: Parameter;
+  disableSearch: boolean;
+  placeholder?: string;
+  disablePKRemappingForSearch?: boolean;
+  options: FieldValue[];
+  valuesMode: ValuesMode;
 }) {
   if (placeholder) {
     return placeholder;
@@ -232,7 +288,12 @@ export function getValuesMode({
   fields,
   disableSearch,
   disablePKRemappingForSearch,
-}) {
+}: {
+  parameter?: Parameter;
+  fields: Field[];
+  disableSearch: boolean;
+  disablePKRemappingForSearch?: boolean;
+}): ValuesMode {
   if (
     isSearchable({
       parameter,
@@ -252,7 +313,7 @@ export function getValuesMode({
   return "none";
 }
 
-export function isNumeric(field, parameter) {
+export function isNumeric(field: Field, parameter?: Parameter) {
   if (parameter) {
     return isNumberParameter(parameter);
   }