diff --git a/frontend/src/metabase-lib/lib/metadata/Field.ts b/frontend/src/metabase-lib/lib/metadata/Field.ts
index 3394eb1f694bed198ea28f6d9ac9e45ebf3db6ab..74707a42e5fb5bb84c7ecfac259163451b03b913 100644
--- a/frontend/src/metabase-lib/lib/metadata/Field.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Field.ts
@@ -32,11 +32,14 @@ import {
   getIconForField,
   getFilterOperators,
 } from "metabase/lib/schema_metadata";
-import { FieldFingerprint } from "metabase-types/api/field";
-import { Field as FieldRef } from "metabase-types/types/Query";
 import { FieldDimension } from "../Dimension";
-import Table from "./Table";
 import Base from "./Base";
+import type { FieldFingerprint } from "metabase-types/api/field";
+import type { Field as FieldRef } from "metabase-types/types/Query";
+import type StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
+import type NativeQuery from "metabase-lib/lib/queries/NativeQuery";
+import type Table from "./Table";
+import type Metadata from "./Metadata";
 
 export const LONG_TEXT_MIN = 80;
 
@@ -57,9 +60,14 @@ class FieldInner extends Base {
   fingerprint?: FieldFingerprint;
   base_type: string | null;
   table?: Table;
+  table_id?: Table["id"];
   target?: Field;
   has_field_values?: "list" | "search" | "none";
   values: any[];
+  metadata?: Metadata;
+
+  // added when creating "virtual fields" that are associated with a given query
+  query?: StructuredQuery | NativeQuery;
 
   getId() {
     if (Array.isArray(this.id)) {
diff --git a/frontend/src/metabase-lib/lib/metadata/Metadata.ts b/frontend/src/metabase-lib/lib/metadata/Metadata.ts
index 8d3d14e63d77e0cb433caacc8f0011656416efd4..ded4bbafb41047abd0f12aa7f5adee33b2e54f80 100644
--- a/frontend/src/metabase-lib/lib/metadata/Metadata.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Metadata.ts
@@ -2,8 +2,11 @@
 // @ts-nocheck
 import _ from "underscore";
 import Base from "./Base";
-import Question from "../Question";
-import Database from "./Database";
+import type Question from "../Question";
+import type Database from "./Database";
+import type Table from "./Table";
+import type Schema from "./Schema";
+
 /**
  * @typedef { import("./metadata").DatabaseId } DatabaseId
  * @typedef { import("./metadata").SchemaId } SchemaId
@@ -18,7 +21,9 @@ import Database from "./Database";
  */
 
 export default class Metadata extends Base {
-  databases: Database[];
+  databases: { [databaseId: string]: Database };
+  questions: { [cardId: string]: Question };
+  tables: { [tableId: string]: Table };
 
   /**
    * @deprecated this won't be sorted or filtered in a meaningful way
@@ -76,7 +81,7 @@ export default class Metadata extends Base {
    * @param {DatabaseId} databaseId
    * @returns {?Database}
    */
-  database(databaseId) {
+  database(databaseId): Database | null {
     return (databaseId != null && this.databases[databaseId]) || null;
   }
 
@@ -84,7 +89,7 @@ export default class Metadata extends Base {
    * @param {SchemaId} schemaId
    * @returns {Schema}
    */
-  schema(schemaId) {
+  schema(schemaId): Schema | null {
     return (schemaId != null && this.schemas[schemaId]) || null;
   }
 
@@ -93,7 +98,7 @@ export default class Metadata extends Base {
    * @param {TableId} tableId
    * @returns {?Table}
    */
-  table(tableId) {
+  table(tableId): Table | null {
     return (tableId != null && this.tables[tableId]) || null;
   }
 
@@ -101,11 +106,11 @@ export default class Metadata extends Base {
    * @param {FieldId} fieldId
    * @returns {?Field}
    */
-  field(fieldId) {
+  field(fieldId): Field | null {
     return (fieldId != null && this.fields[fieldId]) || null;
   }
 
-  question(cardId) {
+  question(cardId): Question | null {
     return (cardId != null && this.questions[cardId]) || null;
   }
 
diff --git a/frontend/src/metabase-lib/lib/metadata/Schema.ts b/frontend/src/metabase-lib/lib/metadata/Schema.ts
index 4426e151adfe7b778998faf931816c4fd6492db2..67d62c3901ae075e6d0b7ed4d2e74db86a361000 100644
--- a/frontend/src/metabase-lib/lib/metadata/Schema.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Schema.ts
@@ -2,8 +2,8 @@
 // @ts-nocheck
 import Base from "./Base";
 import { titleize, humanize } from "metabase/lib/formatting";
-import Database from "./Database";
-import Table from "./Table";
+import type Database from "./Database";
+import type Table from "./Table";
 /**
  * Wrapper class for a {@link Database} schema. Contains {@link Table}s.
  */
diff --git a/frontend/src/metabase-lib/lib/metadata/Table.ts b/frontend/src/metabase-lib/lib/metadata/Table.ts
index c2b3ef5b2d57852d58d9d46d8cfb478d0be51672..ee924b2cdb68dfd6acadacbce74f8b4816dd57fa 100644
--- a/frontend/src/metabase-lib/lib/metadata/Table.ts
+++ b/frontend/src/metabase-lib/lib/metadata/Table.ts
@@ -2,13 +2,15 @@
 // @ts-nocheck
 // NOTE: this needs to be imported first due to some cyclical dependency nonsense
 import Question from "../Question";
-import Schema from "./Schema";
 import Base from "./Base";
 import { singularize } from "metabase/lib/formatting";
 import { getAggregationOperators } from "metabase/lib/schema_metadata";
 import { createLookupByProperty, memoizeClass } from "metabase-lib/lib/utils";
-import Field from "./Field";
-
+import type Metadata from "./Metadata";
+import type Schema from "./Schema";
+import type Field from "./Field";
+import type Database from "./Database";
+import type { TableId } from "metabase-types/types/Table";
 /**
  * @typedef { import("./metadata").SchemaName } SchemaName
  * @typedef { import("./metadata").EntityType } EntityType
@@ -18,7 +20,8 @@ import Field from "./Field";
 /** This is the primary way people interact with tables */
 
 class TableInner extends Base {
-  id: number;
+  id: TableId;
+  name: string;
   description?: string;
   fks?: any[];
   schema?: Schema;
@@ -26,6 +29,8 @@ class TableInner extends Base {
   schema_name: string;
   db_id: number;
   fields: Field[];
+  metadata?: Metadata;
+  db?: Database | undefined | null;
 
   hasSchema() {
     return (this.schema_name && this.db && this.db.schemas.length > 1) || false;
diff --git a/frontend/src/metabase-types/api/table.ts b/frontend/src/metabase-types/api/table.ts
index 91e63785d68f8d3d748fb19e7a82ee0f148f04ab..790afe8bdb616a3a097582c90393aa93a1c61987 100644
--- a/frontend/src/metabase-types/api/table.ts
+++ b/frontend/src/metabase-types/api/table.ts
@@ -2,7 +2,9 @@ import { ForeignKey } from "./foreign-key";
 import { Database } from "./database";
 import { Field } from "./field";
 
-export type TableId = number | string; // can be string for virtual questions (e.g. "card__17")
+export type ConcreteTableId = number;
+export type VirtualTableId = string; // e.g. "card__17" where 17 is a card id
+export type TableId = ConcreteTableId | VirtualTableId;
 
 export type VisibilityType =
   | null
diff --git a/frontend/src/metabase-types/types/Table.ts b/frontend/src/metabase-types/types/Table.ts
index 1819378bd55349d57224eefc255eda5d15d7c426..59f0bd8395c0756b88ffa5a8e4abc2f7aa2c104a 100644
--- a/frontend/src/metabase-types/types/Table.ts
+++ b/frontend/src/metabase-types/types/Table.ts
@@ -10,8 +10,10 @@ import { Segment } from "./Segment";
 import { Metric } from "./Metric";
 import { DatabaseId } from "./Database";
 import { ForeignKey } from "../api/foreign-key";
+import { TableId as _TableId } from "metabase-types/api";
+
+export type TableId = _TableId;
 
-export type TableId = number;
 export type SchemaName = string;
 
 type TableVisibilityType = string; // FIXME
diff --git a/frontend/src/metabase/admin/permissions/selectors/confirmations.ts b/frontend/src/metabase/admin/permissions/selectors/confirmations.ts
index a1d5e2621c79bce7a4c1a05146eb416e359de606..f55405ad8ffbae8b3d5aef30ca18501930948de3 100644
--- a/frontend/src/metabase/admin/permissions/selectors/confirmations.ts
+++ b/frontend/src/metabase/admin/permissions/selectors/confirmations.ts
@@ -6,9 +6,13 @@ import {
   getNativePermission,
   getSchemasPermission,
 } from "metabase/admin/permissions/utils/graph";
-import { Group, GroupsPermissions } from "metabase-types/api";
-import { EntityId } from "../types";
-import Database from "metabase-lib/lib/metadata/Database";
+import type {
+  Group,
+  GroupsPermissions,
+  ConcreteTableId,
+} from "metabase-types/api";
+import type { EntityId } from "../types";
+import type Database from "metabase-lib/lib/metadata/Database";
 
 export const getDefaultGroupHasHigherAccessText = (defaultGroup: Group) =>
   t`The "${defaultGroup.name}" group has a higher level of access than this, which will override this setting. You should limit or revoke the "${defaultGroup.name}" group's access to this item.`;
@@ -139,7 +143,7 @@ export function getRevokingAccessToAllTablesWarningModal(
     const allTableEntityIds = database.tables.map(table => ({
       databaseId: table.db_id,
       schemaName: table.schema_name || "",
-      tableId: table.id,
+      tableId: table.id as ConcreteTableId,
     }));
 
     // Show the warning only if user tries to revoke access to the very last table of all schemas
diff --git a/frontend/src/metabase/admin/permissions/selectors/data-permissions/breadcrumbs.ts b/frontend/src/metabase/admin/permissions/selectors/data-permissions/breadcrumbs.ts
index 903713c778d4d84bfeb702c71e8399fadf708ebc..0f69c67425f5e3f66532b34ffb5099a568ed012c 100644
--- a/frontend/src/metabase/admin/permissions/selectors/data-permissions/breadcrumbs.ts
+++ b/frontend/src/metabase/admin/permissions/selectors/data-permissions/breadcrumbs.ts
@@ -1,4 +1,6 @@
-import Metadata from "metabase-lib/lib/metadata/Metadata";
+import type Metadata from "metabase-lib/lib/metadata/Metadata";
+import type Schema from "metabase-lib/lib/metadata/Schema";
+import type Table from "metabase-lib/lib/metadata/Table";
 import { Group } from "metabase-types/api";
 import _ from "underscore";
 
@@ -46,7 +48,7 @@ export const getDatabasesEditorBreadcrumbs = (
     return [groupItem, databaseItem];
   }
 
-  const schema = database.schema(schemaName);
+  const schema = database.schema(schemaName) as Schema;
   const schemaItem = {
     id: schema.name,
     text: schema.name,
@@ -79,7 +81,7 @@ export const getGroupsDataEditorBreadcrumbs = (
     return [databaseItem];
   }
 
-  const schema = database.schema(schemaName);
+  const schema = database.schema(schemaName) as Schema;
   const schemaItem = {
     id: schema.id,
     text: schema.name,
@@ -92,7 +94,7 @@ export const getGroupsDataEditorBreadcrumbs = (
     return [databaseItem, hasMultipleSchemas && schemaItem].filter(Boolean);
   }
 
-  const table = metadata.table(tableId);
+  const table = metadata.table(tableId) as Table;
 
   const tableItem = {
     id: table.id,
diff --git a/frontend/src/metabase/admin/permissions/selectors/data-permissions/permission-editor.ts b/frontend/src/metabase/admin/permissions/selectors/data-permissions/permission-editor.ts
index ddf3bc6dbc428f2e9d2283fefaeabf58c0729014..c57955aaee4c78ae0fd24dee4605a7348125a3a9 100644
--- a/frontend/src/metabase/admin/permissions/selectors/data-permissions/permission-editor.ts
+++ b/frontend/src/metabase/admin/permissions/selectors/data-permissions/permission-editor.ts
@@ -164,7 +164,7 @@ export const getDatabasesPermissionEditor = createSelector(
     if (database && (schemaName != null || hasSingleSchema)) {
       const schema: Schema = hasSingleSchema
         ? database.getSchemas()[0]
-        : database.schema(schemaName);
+        : (database.schema(schemaName) as Schema);
 
       entities = schema
         .getTables()
diff --git a/frontend/src/metabase/admin/permissions/utils/data-entity-id.ts b/frontend/src/metabase/admin/permissions/utils/data-entity-id.ts
index 423c403114bddf99f805fb23d4c0fd4eb95eefc0..240bc6cb6c0ecbbb9a71c84359e613652218e23a 100644
--- a/frontend/src/metabase/admin/permissions/utils/data-entity-id.ts
+++ b/frontend/src/metabase/admin/permissions/utils/data-entity-id.ts
@@ -1,7 +1,8 @@
-import Database from "metabase-lib/lib/metadata/Database";
-import Schema from "metabase-lib/lib/metadata/Schema";
-import Table from "metabase-lib/lib/metadata/Table";
-import { EntityId, PermissionSubject } from "../types";
+import type Database from "metabase-lib/lib/metadata/Database";
+import type Schema from "metabase-lib/lib/metadata/Schema";
+import type Table from "metabase-lib/lib/metadata/Table";
+import type { EntityId, PermissionSubject } from "../types";
+import type { ConcreteTableId } from "metabase-types/api";
 
 export const getDatabaseEntityId = (databaseEntity: Database) => ({
   databaseId: databaseEntity.id,
@@ -15,7 +16,7 @@ export const getSchemaEntityId = (schemaEntity: Schema) => ({
 export const getTableEntityId = (tableEntity: Table) => ({
   databaseId: tableEntity.db_id,
   schemaName: tableEntity.schema_name,
-  tableId: tableEntity.id,
+  tableId: tableEntity.id as ConcreteTableId,
 });
 
 export const isTableEntityId = (entityId: Partial<EntityId>) =>
diff --git a/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts b/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts
index 7914b7e87e3332860a10f2efd116adb3ea95a73a..e5f37bfb0fe94ebcc216fd9efeb014dda5dd145e 100644
--- a/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts
+++ b/frontend/src/metabase/admin/permissions/utils/graph/data-permissions.ts
@@ -5,10 +5,10 @@ import {
   PLUGIN_ADMIN_PERMISSIONS_TABLE_FIELDS_PERMISSION_VALUE,
   PLUGIN_ADVANCED_PERMISSIONS,
 } from "metabase/plugins";
-import { GroupsPermissions } from "metabase-types/api";
-import Database from "metabase-lib/lib/metadata/Database";
-import Table from "metabase-lib/lib/metadata/Table";
-import {
+import type { GroupsPermissions, ConcreteTableId } from "metabase-types/api";
+import type Database from "metabase-lib/lib/metadata/Database";
+import type Table from "metabase-lib/lib/metadata/Table";
+import type {
   DatabaseEntityId,
   DataPermission,
   EntityId,
@@ -202,7 +202,7 @@ export function downgradeNativePermissionsIfNeeded(
 const metadataTableToTableEntityId = (table: Table) => ({
   databaseId: table.db_id,
   schemaName: table.schema_name || "",
-  tableId: table.id,
+  tableId: table.id as ConcreteTableId,
 });
 
 // TODO Atte Keinänen 6/24/17 See if this method could be simplified
diff --git a/frontend/src/metabase/admin/permissions/utils/graph/permissions-diff.ts b/frontend/src/metabase/admin/permissions/utils/graph/permissions-diff.ts
index 0d18ccd3b2c61c05780f29c3c9c4bc164f935181..fb6ab6a738d792cfbc743b1376d303f4d2a17c17 100644
--- a/frontend/src/metabase/admin/permissions/utils/graph/permissions-diff.ts
+++ b/frontend/src/metabase/admin/permissions/utils/graph/permissions-diff.ts
@@ -1,7 +1,11 @@
 import _ from "underscore";
 
-import { Group, GroupsPermissions } from "metabase-types/api";
-import Database from "metabase-lib/lib/metadata/Database";
+import type {
+  Group,
+  GroupsPermissions,
+  ConcreteTableId,
+} from "metabase-types/api";
+import type Database from "metabase-lib/lib/metadata/Database";
 import {
   getFieldsPermission,
   getNativePermission,
@@ -46,7 +50,7 @@ function diffDatabasePermissions(
       {
         databaseId: database.id,
         schemaName: table.schema_name || "",
-        tableId: table.id,
+        tableId: table.id as ConcreteTableId,
       },
       "data",
     );
@@ -56,7 +60,7 @@ function diffDatabasePermissions(
       {
         databaseId: database.id,
         schemaName: table.schema_name || "",
-        tableId: table.id,
+        tableId: table.id as ConcreteTableId,
       },
       "data",
     );
diff --git a/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.tsx b/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.tsx
index bfb09b515257452095535083bcabc35608cc15da..5bb61f47cb6f5f335d238ec0fbf4876c21e8b2b7 100644
--- a/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.tsx
+++ b/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.tsx
@@ -20,7 +20,7 @@ import ConnectedTables from "./ConnectedTables";
 
 type OwnProps = {
   className?: string;
-  tableId: number;
+  tableId: Table["id"];
   onConnectedTableClick?: (table: Table) => void;
 };
 
@@ -33,8 +33,8 @@ const mapStateToProps = (state: any, props: OwnProps): { table?: Table } => {
 };
 
 const mapDispatchToProps: {
-  fetchForeignKeys: (args: { id: number }) => Promise<any>;
-  fetchMetadata: (args: { id: number }) => Promise<any>;
+  fetchForeignKeys: (args: { id: Table["id"] }) => Promise<any>;
+  fetchMetadata: (args: { id: Table["id"] }) => Promise<any>;
 } = {
   fetchForeignKeys: Tables.actions.fetchForeignKeys,
   fetchMetadata: Tables.actions.fetchMetadata,
diff --git a/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx b/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx
index 8f448a882a82a67bfbfbdf8c00dae6e356aac532..da8a66ab179e467fe2c0e3cdd12a39145f836d99 100644
--- a/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx
+++ b/frontend/src/metabase/components/MetadataInfo/TableInfo/TableInfo.unit.spec.tsx
@@ -30,7 +30,7 @@ const tableWithoutDescription = new Table({
 const fetchForeignKeys = jest.fn();
 const fetchMetadata = jest.fn();
 
-function setup({ id, table }: { table: Table | undefined; id: number }) {
+function setup({ id, table }: { table: Table | undefined; id: Table["id"] }) {
   fetchForeignKeys.mockReset();
   fetchMetadata.mockReset();
 
diff --git a/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx b/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx
index aa02c3d4bc0af4624e452ae101a3d94788b69e17..9492d27ca480c40192d7def9180371e6b5da0d72 100644
--- a/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx
+++ b/frontend/src/metabase/visualizations/components/ObjectDetail/ObjectDetail.tsx
@@ -7,7 +7,7 @@ import { isPK } from "metabase/lib/schema_metadata";
 import Table from "metabase-lib/lib/metadata/Table";
 
 import { State } from "metabase-types/store";
-import { ForeignKey } from "metabase-types/api";
+import type { ForeignKey, ConcreteTableId } from "metabase-types/api";
 import { DatasetData } from "metabase-types/types/Dataset";
 import { ObjectId, OnVisualizationClickType } from "./types";
 
@@ -35,6 +35,7 @@ import {
   getCanZoomNextRow,
 } from "metabase/query_builder/selectors";
 import { columnSettings } from "metabase/visualizations/lib/settings/column";
+import { isVirtualCardId } from "metabase/lib/saved-questions";
 
 import {
   getObjectName,
@@ -159,8 +160,8 @@ export function ObjectDetailFn({
       return;
     }
 
-    if (table && table.fks == null) {
-      fetchTableFks(table.id);
+    if (table && table.fks == null && !isVirtualCardId(table.id)) {
+      fetchTableFks(table.id as ConcreteTableId);
     }
     // load up FK references
     if (tableForeignKeys) {