You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ma...@apache.org on 2025/09/20 22:47:48 UTC

(superset) branch master updated: feat: AI-powered TypeScript migration framework with parallel processing (#35045)

This is an automated email from the ASF dual-hosted git repository.

maximebeauchemin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new ecb3ac68ff feat: AI-powered TypeScript migration framework with parallel processing (#35045)
ecb3ac68ff is described below

commit ecb3ac68ff79fe0d60cdecd54f4bab120ee1ca32
Author: Maxime Beauchemin <ma...@gmail.com>
AuthorDate: Sat Sep 20 15:47:42 2025 -0700

    feat: AI-powered TypeScript migration framework with parallel processing (#35045)
    
    Co-authored-by: Claude <no...@anthropic.com>
    Co-authored-by: Mehmet Salih Yavuz <sa...@proton.me>
    Co-authored-by: Elizabeth Thompson <es...@gmail.com>
---
 .claude/commands/js-to-ts.md                       |  10 +
 .claude/projects/js-to-ts/AGENT.md                 | 684 +++++++++++++++++++++
 .claude/projects/js-to-ts/COORDINATOR.md           | 199 ++++++
 .claude/projects/js-to-ts/PROJECT.md               |  76 +++
 .../src/utils/{roundDecimal.js => roundDecimal.ts} |  11 +-
 .../{roundDecimal.test.js => roundDecimal.test.ts} |   0
 .../spec/fixtures/mockDashboardLayout.js           |  20 +-
 .../MessageToasts/{reducers.js => reducers.ts}     |  20 +-
 .../src/components/Tag/Tag.stories.tsx             |   2 +-
 superset-frontend/src/components/Tag/Tag.test.tsx  |   2 +-
 superset-frontend/src/components/Tag/index.tsx     |   2 +-
 superset-frontend/src/components/Tag/utils.tsx     |   4 +-
 .../src/components/TagsList/index.tsx              |   2 +-
 .../src/dashboard/actions/sliceEntities.ts         |  31 +
 .../components/AddSliceCard/AddSliceCard.tsx       |   4 +-
 .../src/dashboard/components/SliceAdder.tsx        |   3 +-
 .../components/dnd/dragDroppableConfig.js          |  85 ---
 .../components/dnd/dragDroppableConfig.ts          | 188 ++++++
 .../DynamicComponent/DynamicComponent.tsx          |   3 +-
 .../gridComponents/new/DraggableNewComponent.tsx   |   3 +-
 ...yDashboardLayout.js => emptyDashboardLayout.ts} |  39 +-
 .../{sliceEntities.js => sliceEntities.ts}         |  35 +-
 .../src/dashboard/util/activeDashboardFilters.js   | 126 ----
 .../src/dashboard/util/activeDashboardFilters.ts   | 211 +++++++
 ...peTreeEntry.js => buildFilterScopeTreeEntry.ts} |  32 +-
 ...sParent.test.js => dropOverflowsParent.test.ts} |  39 +-
 ...opOverflowsParent.js => dropOverflowsParent.ts} |  15 +-
 ...entContainer.js => findFirstParentContainer.ts} |   5 +-
 ...ayout.test.js => getChartIdsFromLayout.test.ts} |  33 +-
 ...nentFromChartId.js => getChartIdsFromLayout.ts} |  25 +-
 ...erScopes.js => getDirectPathToTabIndex.test.ts} |  37 +-
 ...athToTabIndex.js => getDirectPathToTabIndex.ts} |  12 +-
 .../util/{getEmptyLayout.js => getEmptyLayout.ts}  |  18 +-
 .../util/getKeyForFilterScopeTree.test.ts          |  78 +++
 ...terScopeTree.js => getKeyForFilterScopeTree.ts} |   7 +-
 .../util/getLayoutComponentFromChartId.test.ts     | 105 ++++
 ...ChartId.js => getLayoutComponentFromChartId.ts} |   6 +-
 ...IdFromPath.js => getLeafComponentIdFromPath.ts} |   9 +-
 .../src/dashboard/util/isDashboardEmpty.test.ts    |   2 +-
 .../src/dashboard/util/isDashboardLoading.js       |  23 -
 .../src/dashboard/util/isDashboardLoading.test.ts  |  48 ++
 ...ChartIdsFromLayout.js => isDashboardLoading.ts} |  27 +-
 .../util/isInDifferentFilterScopes.test.ts         | 197 ++++++
 ...ilterScopes.js => isInDifferentFilterScopes.ts} |  29 +-
 .../util/logging/childChartsDidLoad.test.ts        | 246 ++++++++
 ...childChartsDidLoad.js => childChartsDidLoad.ts} |  30 +-
 .../dashboard/util/serializeFilterScopes.test.ts   | 113 ++++
 ...izeFilterScopes.js => serializeFilterScopes.ts} |  24 +-
 ...ldWrapChildInRow.js => shouldWrapChildInRow.ts} |  23 +-
 .../explore/components/DatasourcePanel/index.tsx   |   1 +
 .../controls/FilterControl/adhocFilterType.js      |  37 --
 .../controls/FilterControl/adhocFilterType.test.ts | 103 ++++
 .../controls/FilterControl/adhocFilterType.ts      |  64 ++
 .../controls/FilterControl/columnType.test.ts      |  53 ++
 .../columnType.js => FilterControl/columnType.ts}  |  11 +-
 .../components/controls/FilterControl/types.ts     |   7 +
 .../MetricControl/aggregateOptionType.test.ts}     |  38 +-
 .../aggregateOptionType.ts}                        |   9 +-
 .../types.ts => MetricControl/columnType.ts}       |  13 +-
 .../controls/MetricControl/savedMetricType.js      |  25 -
 .../controls/MetricControl/savedMetricType.test.ts |  45 ++
 .../{aggregateOptionType.js => savedMetricType.ts} |   7 +-
 .../components/controls/MetricControl/types.ts     |   4 +
 .../src/explore/controlPanels/Separator.test.ts    |  76 +++
 .../controlPanels/{Separator.js => Separator.ts}   |  23 +-
 ...rides.js => timeGrainSqlaAnimationOverrides.ts} |  17 +-
 superset-frontend/src/types/TagType.ts             |   2 -
 .../src/utils/DebouncedMessageQueue.test.ts        |  66 ++
 ...cedMessageQueue.js => DebouncedMessageQueue.ts} |  31 +-
 superset-frontend/src/utils/datasourceUtils.js     |  27 -
 .../src/utils/datasourceUtils.test.ts              | 190 ++++++
 superset-frontend/src/utils/datasourceUtils.ts     |  57 ++
 .../src/utils/getControlsForVizType.test.ts        | 101 +++
 .../src/utils/getControlsForVizType.ts             |  74 +++
 .../src/utils/hostNamesConfig.test.ts              |  58 ++
 .../{hostNamesConfig.js => hostNamesConfig.ts}     |  10 +-
 superset-frontend/src/utils/reducerUtils.test.ts   | 129 ++++
 superset-frontend/src/utils/reducerUtils.ts        | 107 ++++
 78 files changed, 3815 insertions(+), 513 deletions(-)

diff --git a/.claude/commands/js-to-ts.md b/.claude/commands/js-to-ts.md
new file mode 100644
index 0000000000..b3e0048983
--- /dev/null
+++ b/.claude/commands/js-to-ts.md
@@ -0,0 +1,10 @@
+# JavaScript to TypeScript Migration Command
+
+## Usage
+```
+/js-to-ts <core-filename>
+```
+- `<core-filename>` - Path to CORE file relative to `superset-frontend/` (e.g., `src/utils/common.js`, `src/middleware/loggerMiddleware.js`)
+
+## Agent Instructions
+**See:** [../projects/js-to-ts/AGENT.md](../projects/js-to-ts/AGENT.md) for complete migration guide.
diff --git a/.claude/projects/js-to-ts/AGENT.md b/.claude/projects/js-to-ts/AGENT.md
new file mode 100644
index 0000000000..47e6cac7cf
--- /dev/null
+++ b/.claude/projects/js-to-ts/AGENT.md
@@ -0,0 +1,684 @@
+# JavaScript to TypeScript Migration Agent Guide
+
+**Complete technical reference for converting JavaScript/JSX files to TypeScript/TSX in Apache Superset frontend.**
+
+**Agent Role:** Atomic migration unit - migrate the core file + ALL related tests/mocks as one cohesive unit. Use `git mv` to preserve history, NO `git commit`. NO global import changes. Report results upon completion.
+
+---
+
+## 🎯 Migration Principles
+
+1. **Atomic migration units** - Core file + all related tests/mocks migrate together
+2. **Zero `any` types** - Use proper TypeScript throughout
+3. **Leverage existing types** - Reuse established definitions
+4. **Type inheritance** - Derivatives extend base component types
+5. **Strategic placement** - File types for maximum discoverability
+6. **Surgical improvements** - Enhance existing types during migration
+
+---
+
+## Step 0: Dependency Check (MANDATORY)
+
+**Command:**
+```bash
+grep -E "from '\.\./.*\.jsx?'|from '\./.*\.jsx?'|from 'src/.*\.jsx?'" superset-frontend/{filename}
+```
+
+**Decision:**
+- βœ… No matches β†’ Proceed with atomic migration (core + tests + mocks)
+- ❌ Matches found β†’ EXIT with dependency report (see format below)
+
+---
+
+## Step 1: Identify Related Files (REQUIRED)
+
+**Atomic Migration Scope:**
+For core file `src/utils/example.js`, also migrate:
+- `src/utils/example.test.js` / `src/utils/example.test.jsx`
+- `src/utils/example.spec.js` / `src/utils/example.spec.jsx`
+- `src/utils/__mocks__/example.js`
+- Any other related test/mock files found by pattern matching
+
+**Find all related test and mock files:**
+```bash
+# Pattern-based search for related files
+basename=$(basename {filename} .js)
+dirname=$(dirname superset-frontend/{filename})
+
+# Find test files
+find "$dirname" -name "${basename}.test.js" -o -name "${basename}.test.jsx"
+find "$dirname" -name "${basename}.spec.js" -o -name "${basename}.spec.jsx"
+
+# Find mock files  
+find "$dirname" -name "__mocks__/${basename}.js"
+find "$dirname" -name "${basename}.mock.js"
+```
+
+**Migration Requirement:** All discovered related files MUST be migrated together as one atomic unit.
+
+**Test File Creation:** If NO test files exist for the core file, CREATE a minimal test file using the following pattern:
+- Location: Same directory as core file
+- Name: `{basename}.test.ts` (e.g., `DebouncedMessageQueue.test.ts`)
+- Content: Basic test structure importing and testing the main functionality
+- Use proper TypeScript types in test file
+
+---
+
+## πŸ—ΊοΈ Type Reference Map
+
+### From `@superset-ui/core`
+```typescript
+// Data & Query
+QueryFormData, QueryData, JsonObject, AnnotationData, AdhocMetric
+LatestQueryFormData, GenericDataType, DatasourceType, ExtraFormData
+DataMaskStateWithId, NativeFilterScope, NativeFiltersState, NativeFilterTarget
+
+// UI & Theme  
+FeatureFlagMap, LanguagePack, ColorSchemeConfig, SequentialSchemeConfig
+```
+
+### From `@superset-ui/chart-controls`
+```typescript
+Dataset, ColumnMeta, ControlStateMapping
+```
+
+### From Local Types (`src/types/`)
+```typescript
+// Authentication
+User, UserWithPermissionsAndRoles, BootstrapUser, PermissionsAndRoles
+
+// Dashboard
+Dashboard, DashboardState, DashboardInfo, DashboardLayout, LayoutItem
+ComponentType, ChartConfiguration, ActiveFilters
+
+// Charts
+Chart, ChartState, ChartStatus, ChartLinkedDashboard, Slice, SaveActionType
+
+// Data
+Datasource, Database, Owner, Role
+
+// UI Components
+TagType, FavoriteStatus, Filter, ImportResourceName
+```
+
+### From Domain Types
+```typescript
+// src/dashboard/types.ts
+RootState, ChartsState, DatasourcesState, FilterBarOrientation
+ChartCrossFiltersConfig, ActiveTabs, MenuKeys
+
+// src/explore/types.ts  
+ExplorePageInitialData, ExplorePageState, ExploreResponsePayload, OptionSortType
+
+// src/SqlLab/types.ts
+[SQL Lab specific types]
+```
+
+---
+
+## πŸ—οΈ Type Organization Strategy
+
+### Type Placement Hierarchy
+
+1. **Component-Colocated** (90% of cases)
+   ```typescript
+   // Same file as component
+   interface MyComponentProps {
+     title: string;
+     onClick: () => void;
+   }
+   ```
+
+2. **Feature-Shared**
+   ```typescript
+   // src/[domain]/components/[Feature]/types.ts
+   export interface FilterConfiguration {
+     filterId: string;
+     targets: NativeFilterTarget[];
+   }
+   ```
+
+3. **Domain-Wide**
+   ```typescript
+   // src/[domain]/types.ts
+   export interface ExploreFormData extends QueryFormData {
+     viz_type: string;
+   }
+   ```
+
+4. **Global**
+   ```typescript
+   // src/types/[TypeName].ts
+   export interface ApiResponse<T> {
+     result: T;
+     count?: number;
+   }
+   ```
+
+### Type Discovery Commands
+```bash
+# Search existing types before creating
+find superset-frontend/src -name "types.ts" -exec grep -l "[TypeConcept]" {} \;
+grep -r "interface.*Props\|type.*Props" superset-frontend/src/
+```
+
+### Derivative Component Patterns
+
+**Rule:** Components that extend others should extend their type interfaces.
+
+```typescript
+// βœ… Base component type
+interface SelectProps {
+  value: string | number;
+  options: SelectOption[];
+  onChange: (value: string | number) => void;
+  disabled?: boolean;
+}
+
+// βœ… Derivative extends base
+interface ChartSelectProps extends SelectProps {
+  charts: Chart[];
+  onChartSelect: (chart: Chart) => void;
+}
+
+// βœ… Derivative with modified props  
+interface DatabaseSelectProps extends Omit<SelectProps, 'value' | 'onChange'> {
+  value: number;  // Narrowed type
+  onChange: (databaseId: number) => void;  // Specific signature
+}
+```
+
+**Common Patterns:**
+- **Extension:** `extends BaseProps` - adds new props
+- **Omission:** `Omit<BaseProps, 'prop'>` - removes props
+- **Modification:** `Omit<BaseProps, 'prop'> & { prop: NewType }` - changes prop type
+- **Restriction:** Override with narrower types (union β†’ specific)
+
+---
+
+## πŸ“‹ Migration Recipe
+
+### Step 2: File Conversion
+```bash
+# Use git mv to preserve history
+git mv component.js component.ts
+git mv Component.jsx Component.tsx
+```
+
+### Step 3: Import & Type Setup
+```typescript
+// Import order (enforced by linting)
+import { FC, ReactNode } from 'react';
+import { JsonObject, QueryFormData } from '@superset-ui/core';
+import { Dataset } from '@superset-ui/chart-controls';
+import type { Dashboard } from 'src/types/Dashboard';
+```
+
+### Step 4: Function & Component Typing
+```typescript
+// Functions with proper parameter/return types
+export function processData(
+  data: Dataset[],
+  config: JsonObject
+): ProcessedData[] {
+  // implementation
+}
+
+// Component props with inheritance
+interface ComponentProps extends BaseProps {
+  data: Chart[];
+  onSelect: (id: number) => void;
+}
+
+const Component: FC<ComponentProps> = ({ data, onSelect }) => {
+  // implementation
+};
+```
+
+### Step 5: State & Redux Typing
+```typescript
+// Hooks with specific types
+const [data, setData] = useState<Chart[]>([]);
+const [selected, setSelected] = useState<number | null>(null);
+
+// Redux with existing RootState
+const mapStateToProps = (state: RootState) => ({
+  charts: state.charts,
+  user: state.user,
+});
+```
+
+---
+
+## 🧠 Type Debugging Strategies (Real-World Learnings)
+
+### The Evolution of Type Approaches
+When you hit type errors, follow this debugging evolution:
+
+#### 1. ❌ Idealized Union Types (First Attempt)
+```typescript
+// Looks clean but doesn't match reality
+type DatasourceInput = Datasource | QueryEditor;
+```
+**Problem**: Real calling sites pass variations, not exact types.
+
+#### 2. ❌ Overly Precise Types (Second Attempt)
+```typescript
+// Tried to match exact calling signatures
+type DatasourceInput =
+  | IDatasource  // From DatasourcePanel
+  | (QueryEditor & { columns: ColumnMeta[] });  // From SaveQuery
+```
+**Problem**: Too rigid, doesn't handle legacy variations.
+
+#### 3. βœ… Flexible Interface (Final Solution)
+```typescript
+// Captures what the function actually needs
+interface DatasourceInput {
+  name?: string | null;  // Allow null for compatibility
+  datasource_name?: string | null;  // Legacy variations
+  columns?: any[];  // Multiple column types accepted
+  database?: { id?: number };
+  // ... other optional properties
+}
+```
+**Success**: Works with all calling sites, focuses on function needs.
+
+### Type Debugging Process
+1. **Start with compilation errors** - they show exact mismatches
+2. **Examine actual usage** - look at calling sites, not idealized types  
+3. **Build flexible interfaces** - capture what functions need, not rigid contracts
+4. **Iterate based on downstream validation** - let calling sites guide your types
+
+---
+
+## 🚨 Anti-Patterns to Avoid
+
+```typescript
+// ❌ Never use any
+const obj: any = {};
+
+// βœ… Use proper types
+const obj: Record<string, JsonObject> = {};
+
+// ❌ Don't recreate base component props
+interface ChartSelectProps {
+  value: string;     // Duplicated from SelectProps
+  onChange: () => void;  // Duplicated from SelectProps
+  charts: Chart[];   // New prop
+}
+
+// βœ… Inherit and extend
+interface ChartSelectProps extends SelectProps {
+  charts: Chart[];   // Only new props
+}
+
+// ❌ Don't create ad-hoc type variations
+interface UserInfo {
+  name: string;
+  email: string;
+}
+
+// βœ… Extend existing types (DRY principle)
+import { User } from 'src/types/bootstrapTypes';
+type UserDisplayInfo = Pick<User, 'firstName' | 'lastName' | 'email'>;
+
+// ❌ Don't create overly rigid unions
+type StrictInput = ExactTypeA | ExactTypeB;
+
+// βœ… Create flexible interfaces for function parameters
+interface FlexibleInput {
+  // Focus on what the function actually needs
+  commonProperty: string;
+  optionalVariations?: any;  // Allow for legacy variations
+}
+```
+
+## πŸ“ DRY Type Guidelines (WHERE TYPES BELONG)
+
+### Type Placement Rules
+**CRITICAL**: Type variations must live close to where they belong, not scattered across files.
+
+#### βœ… Proper Type Organization
+```typescript
+// ❌ Don't create one-off interfaces in utility files
+// src/utils/datasourceUtils.ts
+interface DatasourceInput { /* custom interface */ }  // Wrong!
+
+// βœ… Use existing types or extend them in their proper domain
+// src/utils/datasourceUtils.ts
+import { IDatasource } from 'src/explore/components/DatasourcePanel';
+import { QueryEditor } from 'src/SqlLab/types';
+
+// Create flexible interface that references existing types
+interface FlexibleDatasourceInput {
+  // Properties that actually exist across variations
+}
+```
+
+#### Type Location Hierarchy
+1. **Domain Types**: `src/{domain}/types.ts` (dashboard, explore, SqlLab)
+2. **Component Types**: Co-located with components  
+3. **Global Types**: `src/types/` directory
+4. **Utility Types**: Only when they truly don't belong elsewhere
+
+#### βœ… DRY Type Patterns
+```typescript
+// βœ… Extend existing domain types
+interface SaveQueryData extends Pick<QueryEditor, 'sql' | 'dbId' | 'catalog'> {
+  columns: ColumnMeta[];  // Add what's needed
+}
+
+// βœ… Create flexible interfaces for cross-domain utilities
+interface CrossDomainInput {
+  // Common properties that exist across different source types
+  name?: string | null;  // Accommodate legacy null values
+  // Only include properties the function actually uses
+}
+```
+
+---
+
+## 🎯 PropTypes Auto-Generation (Elegant Approach)
+
+**IMPORTANT**: Superset has `babel-plugin-typescript-to-proptypes` configured to automatically generate PropTypes from TypeScript interfaces. Use this instead of manual PropTypes duplication!
+
+### ❌ Manual PropTypes Duplication (Avoid This)
+```typescript
+export interface MyComponentProps {
+  title: string;
+  count?: number;
+}
+
+// 8+ lines of manual PropTypes duplication 😱
+const propTypes = PropTypes.shape({
+  title: PropTypes.string.isRequired,
+  count: PropTypes.number,
+});
+
+export default propTypes;
+```
+
+### βœ… Auto-Generated PropTypes (Use This)
+```typescript
+import { InferProps } from 'prop-types';
+
+export interface MyComponentProps {
+  title: string;
+  count?: number;
+}
+
+// Single validator function - babel plugin auto-generates PropTypes! ✨
+export default function MyComponentValidator(props: MyComponentProps) {
+  return null; // PropTypes auto-assigned by babel-plugin-typescript-to-proptypes
+}
+
+// Optional: For consumers needing PropTypes type inference
+export type MyComponentPropsInferred = InferProps<typeof MyComponentValidator>;
+```
+
+### Migration Pattern for Type-Only Files
+
+**When migrating type-only files with manual PropTypes:**
+
+1. **Keep the TypeScript interfaces** (single source of truth)
+2. **Replace manual PropTypes** with validator function
+3. **Remove PropTypes imports** and manual shape definitions
+4. **Add InferProps import** if type inference needed
+
+**Example Migration:**
+```typescript
+// Before: 25+ lines with manual PropTypes duplication
+export interface AdhocFilterType { /* ... */ }
+const adhocFilterTypePropTypes = PropTypes.oneOfType([...]);
+
+// After: 3 lines with auto-generation
+export interface AdhocFilterType { /* ... */ }
+export default function AdhocFilterValidator(props: { filter: AdhocFilterType }) {
+  return null; // Auto-generated PropTypes by babel plugin
+}
+```
+
+### Component PropTypes Pattern
+
+**For React components, the babel plugin works automatically:**
+
+```typescript
+interface ComponentProps {
+  title: string;
+  onClick: () => void;
+}
+
+const MyComponent: FC<ComponentProps> = ({ title, onClick }) => {
+  // Component implementation
+};
+
+// PropTypes automatically generated by babel plugin - no manual work needed!
+export default MyComponent;
+```
+
+### Auto-Generation Benefits
+
+- βœ… **Single source of truth**: TypeScript interfaces drive PropTypes
+- βœ… **No duplication**: Eliminate 15-20 lines of manual PropTypes code
+- βœ… **Automatic updates**: Changes to TypeScript automatically update PropTypes
+- βœ… **Type safety**: Compile-time checking ensures PropTypes match interfaces
+- βœ… **Backward compatibility**: Existing JavaScript components continue working
+
+### Babel Plugin Configuration
+
+The plugin is already configured in `babel.config.js`:
+```javascript
+['babel-plugin-typescript-to-proptypes', { loose: true }]
+```
+
+**No additional setup required** - just use TypeScript interfaces and the plugin handles the rest!
+
+---
+
+## πŸ§ͺ Test File Migration Patterns
+
+### Test File Priority
+- **Always migrate test files** alongside production files
+- **Test files are often leaf nodes** - good starting candidates
+- **Create tests if missing** - Leverage new TypeScript types for better test coverage
+
+### Test-Specific Type Patterns
+```typescript
+// Mock interfaces for testing
+interface MockStore {
+  getState: () => Partial<RootState>;  // Partial allows minimal mocking
+}
+
+// Type-safe mocking for complex objects
+const mockDashboardInfo: Partial<DashboardInfo> as DashboardInfo = {
+  id: 123,
+  json_metadata: '{}',
+};
+
+// Sinon stub typing
+let postStub: sinon.SinonStub;
+beforeEach(() => {
+  postStub = sinon.stub(SupersetClient, 'post');
+});
+
+// Use stub reference instead of original method
+expect(postStub.callCount).toBe(1);
+expect(postStub.getCall(0).args[0].endpoint).toMatch('/api/');
+```
+
+### Test Migration Recipe
+1. **Migrate production file first** (if both need migration)
+2. **Update test imports** to point to `.ts/.tsx` files
+3. **Add proper mock typing** using `Partial<T> as T` pattern
+4. **Fix stub typing** - Use stub references, not original methods
+5. **Verify all tests pass** with TypeScript compilation
+
+---
+
+## πŸ”§ Type Conflict Resolution
+
+### Multiple Type Definitions Issue
+**Problem**: Same type name defined in multiple files causes compilation errors.
+
+**Example**: `DashboardInfo` defined in both:
+- `src/dashboard/reducers/types.ts` (minimal)
+- `src/dashboard/components/Header/types.ts` (different shape)  
+- `src/dashboard/types.ts` (complete - used by RootState)
+
+### Resolution Strategy
+1. **Identify the authoritative type**:
+   ```bash
+   # Find which type is used by RootState/main interfaces
+   grep -r "DashboardInfo" src/dashboard/types.ts
+   ```
+
+2. **Use import from authoritative source**:
+   ```typescript
+   // βœ… Import from main domain types
+   import { RootState, DashboardInfo } from 'src/dashboard/types';
+
+   // ❌ Don't import from component-specific files
+   import { DashboardInfo } from 'src/dashboard/components/Header/types';
+   ```
+
+3. **Mock complex types in tests**:
+   ```typescript
+   // For testing - provide minimal required fields
+   const mockInfo: Partial<DashboardInfo> as DashboardInfo = {
+     id: 123,
+     json_metadata: '{}',
+     // Only provide fields actually used in test
+   };
+   ```
+
+### Type Hierarchy Discovery Commands
+```bash
+# Find all definitions of a type
+grep -r "interface.*TypeName\|type.*TypeName" src/
+
+# Find import usage patterns
+grep -r "import.*TypeName" src/
+
+# Check what RootState uses
+grep -A 10 -B 10 "TypeName" src/*/types.ts
+```
+
+---
+
+## Agent Constraints (CRITICAL)
+
+1. **Use git mv** - Run `git mv file.js file.ts` to preserve git history, but NO `git commit`
+2. **NO global import changes** - Don't update imports across codebase
+3. **Type files OK** - Can modify existing type files to improve/align types
+4. **Single-File TypeScript Validation** (CRITICAL) - tsc has known issues with multi-file compilation:
+   - **Core Issue**: TypeScript's `tsc` has documented problems validating multiple files simultaneously in complex projects
+   - **Solution**: ALWAYS validate files one at a time using individual `tsc` calls
+   - **Command Pattern**: `cd superset-frontend && npx tscw --noEmit --allowJs --composite false --project tsconfig.json {single-file-path}`
+   - **Why**: Multi-file validation can produce false positives, miss real errors, and conflict during parallel agent execution
+5. **Downstream Impact Validation** (CRITICAL) - Your migration affects calling sites:
+   - **Find downstream files**: `find superset-frontend/src -name "*.tsx" -o -name "*.ts" | xargs grep -l "your-core-filename" 2>/dev/null || echo "No files found"`
+   - **Validate each downstream file individually**: `cd superset-frontend && npx tscw --noEmit --allowJs --composite false --project tsconfig.json {each-downstream-file}`
+   - **Fix type mismatches** you introduced in calling sites
+   - **NEVER ignore downstream errors** - they indicate your types don't match reality
+6. **Avoid Project-Wide Validation During Migration**:
+   - **NEVER use `npm run type`** during parallel agent execution - produces unreliable results
+   - **Single-file validation is authoritative** - trust individual file checks over project-wide scans
+6. **ESLint validation** - Run `npm run eslint -- --fix {file}` for each migrated file to auto-fix formatting/linting issues
+6. Zero `any` types - use proper TypeScript types
+7. Search existing types before creating new ones
+8. Follow patterns from this guide
+
+---
+
+## Success Report Format
+
+```
+SUCCESS: Atomic Migration of {core-filename}
+
+## Files Migrated (Atomic Unit)
+- Core: {core-filename} β†’ {core-filename.ts/tsx}
+- Tests: {list-of-test-files} β†’ {list-of-test-files.ts/tsx} OR "CREATED: {basename}.test.ts"
+- Mocks: {list-of-mock-files} β†’ {list-of-mock-files.ts}
+- Type files modified: {list-of-type-files}
+
+## Types Created/Improved
+- {TypeName}: {location} ({scope}) - {rationale}
+- {ExistingType}: enhanced in {location} - {improvement-description}
+
+## Documentation Recommendations  
+- ADD_TO_DIRECTORY: {TypeName} - {reason}
+- NO_DOCUMENTATION: {TypeName} - {reason}
+
+## Quality Validation
+- **Single-File TypeScript Validation**: βœ… PASS - Core files individually validated
+  - Core file: `npx tscw --noEmit --allowJs --composite false --project tsconfig.json {core-file}`
+  - Test files: `npx tscw --noEmit --allowJs --composite false --project tsconfig.json {test-file}` (if exists)
+- **Downstream Impact Check**: βœ… PASS - Found {N} files importing this module, all validate individually
+  - Downstream files: {list-of-files-that-import-your-module}
+  - Individual validation: `npx tscw --noEmit --allowJs --composite false --project tsconfig.json {each-downstream-file}`
+- **ESLint validation**: βœ… PASS (using `npm run eslint -- --fix {files}` to auto-fix formatting)
+- **Zero any types**: βœ… PASS
+- **Local imports resolved**: βœ… PASS  
+- **Functionality preserved**: βœ… PASS
+- **Tests pass** (if test file): βœ… PASS
+- **Follow-up action required**: {YES/NO}
+
+## Validation Strategy Notes
+- **Single-file approach used**: Avoided multi-file tsc validation due to known TypeScript compilation issues
+- **Project-wide validation skipped**: `npm run type` not used during parallel migration to prevent false positives
+
+## Migration Learnings
+- Type conflicts encountered: {describe any multiple type definitions}
+- Mock patterns used: {describe test mocking approaches}
+- Import hierarchy decisions: {note authoritative type sources used}
+- PropTypes strategy: {AUTO_GENERATED via babel plugin | MANUAL_DUPLICATION_REMOVED | N/A}
+
+## Improvement Suggestions for Documentation
+- AGENT.md enhancement: {suggest additions to migration guide}
+- Common pattern identified: {note reusable patterns for future migrations}
+```
+
+---
+
+## Dependency Block Report Format
+
+```
+DEPENDENCY_BLOCK: Cannot migrate {filename}
+
+## Blocking Dependencies
+- {path}: {type} - {usage} - {priority}
+
+## Impact Analysis
+- Estimated types: {number}
+- Expected locations: {list}
+- Cross-domain: {YES/NO}
+
+## Recommended Order
+{ordered-list}
+```
+
+---
+
+## πŸ“š Quick Reference
+
+**Type Utilities:**
+- `Record<K, V>` - Object with specific key/value types
+- `Partial<T>` - All properties optional
+- `Pick<T, K>` - Subset of properties
+- `Omit<T, K>` - Exclude specific properties
+- `NonNullable<T>` - Exclude null/undefined
+
+**Event Types:**
+- `MouseEvent<HTMLButtonElement>`
+- `ChangeEvent<HTMLInputElement>`
+- `FormEvent<HTMLFormElement>`
+
+**React Types:**
+- `FC<Props>` - Functional component
+- `ReactNode` - Any renderable content
+- `CSSProperties` - Style objects
+
+---
+
+**Remember:** Every type should add value and clarity. The goal is meaningful type safety that catches bugs and improves developer experience.
diff --git a/.claude/projects/js-to-ts/COORDINATOR.md b/.claude/projects/js-to-ts/COORDINATOR.md
new file mode 100644
index 0000000000..4d225a26dd
--- /dev/null
+++ b/.claude/projects/js-to-ts/COORDINATOR.md
@@ -0,0 +1,199 @@
+# JS-to-TS Coordinator Workflow
+
+**Role:** Strategic migration coordination - select leaf-node files, trigger agents, review results, handle integration, manage dependencies.
+
+---
+
+## 1. Core File Selection Strategy
+
+**Target ONLY Core Files**: Coordinators identify core files (production code), agents handle related tests/mocks atomically.
+
+**File Analysis Commands**:
+```bash
+# Find CORE files with no JS/JSX dependencies (exclude tests/mocks) - SIZE PRIORITIZED
+find superset-frontend/src -name "*.js" -o -name "*.jsx" | grep -v "test\|spec\|mock" | xargs wc -l | sort -n | head -20
+
+# Alternative: Get file sizes in lines with paths
+find superset-frontend/src -name "*.js" -o -name "*.jsx" | grep -v "test\|spec\|mock" | while read file; do
+  lines=$(wc -l < "$file")
+  echo "$lines $file"
+done | sort -n | head -20
+
+# Check dependencies for core files only (start with smallest)
+for file in <core-files-sorted-by-size>; do
+  echo "=== $file ($(wc -l < "$file") lines) ==="
+  grep -E "from '\.\./.*\.jsx?'|from '\./.*\.jsx?'|from 'src/.*\.jsx?'" "$file" || echo "βœ… LEAF CANDIDATE"
+done
+
+# Identify heavily imported files (migrate last)  
+grep -r "from.*utils/common" superset-frontend/src/ | wc -l
+
+# Quick leaf analysis with size priority
+find superset-frontend/src -name "*.js" -o -name "*.jsx" | grep -v "test\|spec\|mock" | head -30 | while read file; do
+  deps=$(grep -E "from '\.\./.*\.jsx?'|from '\./.*\.jsx?'|from 'src/.*\.jsx?'" "$file" | wc -l)
+  lines=$(wc -l < "$file")
+  if [ "$deps" -eq 0 ]; then
+    echo "βœ… LEAF: $lines lines - $file"
+  fi
+done | sort -n
+```
+
+**Priority Order** (Smallest files first for easier wins):
+1. **Small leaf files** (<50 lines) - No JS/JSX imports, quick TypeScript conversion
+2. **Medium leaf files** (50-200 lines) - Self-contained utilities and helpers  
+3. **Small dependency files** (<100 lines) - Import only already-migrated files
+4. **Larger components** (200+ lines) - Complex but well-contained functionality
+5. **Core foundational files** (utils/common.js, controls.jsx) - migrate last regardless of size
+
+**Size-First Benefits**:
+- Faster completion builds momentum
+- Earlier validation of migration patterns
+- Easier rollback if issues arise
+- Better success rate for agent learning
+
+**Migration Unit**: Each agent call migrates:
+- 1 core file (primary target)
+- All related `*.test.js/jsx` files
+- All related `*.mock.js` files  
+- All related `__mocks__/` files
+
+---
+
+## 2. Task Creation & Agent Control
+
+### Task Triggering
+When triggering the `/js-to-ts` command:
+- **Task Title**: Use the core filename as the task title (e.g., "DebouncedMessageQueue.js migration", "hostNamesConfig.js migration")
+- **Task Description**: Include the full relative path to help agent locate the file
+- **Reference**: Point agent to [AGENT.md](./AGENT.md) for technical instructions
+
+### Post-Processing Workflow
+After each agent completes:
+
+1. **Review Agent Report**: Always read and analyze the complete agent report
+2. **Share Summary**: Provide user with key highlights from agent's work:
+   - Files migrated (core + tests/mocks)
+   - Types created or improved
+   - Any validation issues or coordinator actions needed
+3. **Quality Assessment**: Evaluate agent's TypeScript implementation against criteria:
+   - βœ… **Type Usage**: Proper types used, no `any` types
+   - βœ… **Type Filing**: Types placed in correct hierarchy (component β†’ feature β†’ domain β†’ global)
+   - βœ… **Side Effects**: No unintended changes to other files
+   - βœ… **Import Alignment**: Proper .ts/.tsx import extensions
+4. **Integration Decision**:
+   - **COMMIT**: If agent work is complete and high quality
+   - **FIX & COMMIT**: If minor issues need coordinator fixes
+   - **ROLLBACK**: If major issues require complete rework
+5. **Next Action**: Ask user preference - commit this work or trigger next migration
+
+---
+
+## 3. Integration Decision Framework
+
+**Automatic Integration** βœ…:
+- `npm run type` passes without errors
+- Agent created clean TypeScript with proper types
+- Types appropriately filed in hierarchy
+
+**Coordinator Integration** (Fix Side-Effects) πŸ”§:
+- `npm run type` fails BUT agent's work is high quality
+- Good type usage, proper patterns, well-organized
+- Side-effects are manageable TypeScript compilation errors
+- **Coordinator Action**: Integrate the change, then fix global compilation issues
+
+**Rollback Only** ❌:
+- Agent introduced `any` types or poor type choices
+- Types poorly organized or conflicting with existing patterns
+- Fundamental approach issues requiring complete rework
+
+**Integration Process**:
+1. **Review**: Agent already used `git mv` to preserve history
+2. **Fix Side-Effects**: Update dependent files with proper import extensions
+3. **Resolve Types**: Fix any cascading type issues across codebase
+4. **Validate**: Ensure `npm run type` passes after fixes
+
+---
+
+## 4. Common Integration Patterns
+
+**Common Side-Effects (Expect These)**:
+- **Type import conflicts**: Multiple definitions of same type name
+- **Mock object typing**: Tests need complete type satisfaction
+- **Stub method references**: Use stub vars instead of original methods
+
+**Coordinator Fixes (Standard Process)**:
+1. **Import Resolution**:
+   ```bash
+   # Find authoritative type source
+   grep -r "TypeName" src/*/types.ts
+   # Import from domain types (src/dashboard/types.ts) not component types
+   ```
+
+2. **Test Mock Completion**:
+   ```typescript
+   // Use Partial<T> as T pattern for minimal mocking
+   const mockDashboard: Partial<DashboardInfo> as DashboardInfo = {
+     id: 123,
+     json_metadata: '{}',
+   };
+   ```
+
+3. **Stub Reference Fixes**:
+   ```typescript
+   // βœ… Use stub variable
+   expect(postStub.callCount).toBe(1);
+   // ❌ Don't use original method
+   expect(SupersetClient.post.callCount).toBe(1);
+   ```
+
+4. **Validation Commands**:
+   ```bash
+   npm run type          # TypeScript compilation
+   npm test -- filename  # Test functionality
+   git status           # Should show rename, not add/delete
+   ```
+
+---
+
+## 5. File Categories for Planning
+
+### Leaf Files (Start Here)
+**Self-contained files with minimal JS/JSX dependencies**:
+- Test files (80 files) - Usually only import the file being tested
+- Utility files without internal dependencies
+- Components importing only external libraries
+
+### Heavily Imported Files (Migrate Last)
+**Core files that many others depend on**:
+- `utils/common.js` - Core utility functions
+- `utils/reducerUtils.js` - Redux helpers  
+- `@superset-ui/core` equivalent files
+- Major state management files (`explore/store.js`, `dashboard/actions/`)
+
+### Complex Components (Middle Priority)  
+**Large files requiring careful type analysis**:
+- `components/Datasource/DatasourceEditor.jsx` (1,809 lines)
+- `explore/components/controls/AnnotationLayerControl/AnnotationLayer.jsx` (1,031 lines)
+- `explore/components/ExploreViewContainer/index.jsx` (911 lines)
+
+---
+
+## 6. Success Metrics & Continuous Improvement
+
+**Per-File Gates**:
+- βœ… `npm run type` passes after each migration
+- βœ… Zero `any` types introduced
+- βœ… All imports properly typed
+- βœ… Types filed in correct hierarchy
+
+**Linear Scheduling**:
+When agents report `DEPENDENCY_BLOCK`:
+- Queue dependencies in linear order
+- Process one file at a time to avoid conflicts
+- Handle cascading type changes between files
+
+**After Each Migration**:
+1. **Update guides** with new patterns discovered
+2. **Document coordinator fixes** that become common
+3. **Enhance agent instructions** based on recurring issues
+4. **Track success metrics** - automatic vs coordinator integration rates
diff --git a/.claude/projects/js-to-ts/PROJECT.md b/.claude/projects/js-to-ts/PROJECT.md
new file mode 100644
index 0000000000..c8a7b80e62
--- /dev/null
+++ b/.claude/projects/js-to-ts/PROJECT.md
@@ -0,0 +1,76 @@
+# JavaScript to TypeScript Migration Project
+
+Progressive migration of 219 JS/JSX files to TypeScript in Apache Superset frontend.
+
+## πŸ“ Project Documentation
+
+- **[AGENT.md](./AGENT.md)** - Complete technical migration guide for agents (includes type reference, patterns, validation)
+- **[COORDINATOR.md](./COORDINATOR.md)** - Strategic workflow for coordinators (file selection, task management, integration)
+
+## 🎯 Quick Start
+
+**For Agents:** Read [AGENT.md](./AGENT.md) for complete migration instructions
+**For Coordinators:** Read [COORDINATOR.md](./COORDINATOR.md) for workflow and [AGENT.md](./AGENT.md) for supervision
+
+**Command:** `/js-to-ts <filename>` - See [../../commands/js-to-ts.md](../../commands/js-to-ts.md)
+
+## πŸ“Š Migration Progress
+
+**Scope**: 219 files total (112 JS + 107 JSX)
+- Production files: 139 (63%)  
+- Test files: 80 (37%)
+
+**Strategy**: Leaf-first migration with dependency-aware coordination
+
+### Completed Migrations βœ…
+
+1. **roundDecimal** - `plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.js`
+   - Migrated core + test files
+   - Added proper TypeScript function signature with optional precision parameter
+   - All tests pass
+
+2. **timeGrainSqlaAnimationOverrides** - `src/explore/controlPanels/timeGrainSqlaAnimationOverrides.js`
+   - Migrated to TypeScript with ControlPanelState and Dataset types
+   - Added TimeGrainOverrideState interface for return type
+   - Used type guards for safe property access
+
+3. **DebouncedMessageQueue** - `src/utils/DebouncedMessageQueue.js`
+   - Migrated to TypeScript with proper generics
+   - Created DebouncedMessageQueueOptions interface
+   - **CREATED test file** with 4 comprehensive test cases
+   - Excellent class property typing with private/readonly modifiers
+
+**Files Migrated**: 3/219 (1.4%)
+**Tests Created**: 2 (roundDecimal had existing, DebouncedMessageQueue created)
+
+### Next Candidates (Leaf Nodes) 🎯
+
+**Identified leaf files with no JS/JSX dependencies:**
+- `src/utils/hostNamesConfig.js` - Domain configuration utility
+- `src/explore/controlPanels/Separator.js` - Control panel configuration  
+- `src/middleware/loggerMiddleware.js` - Logging middleware
+
+**Migration Quality**: All completed migrations have:
+- βœ… Zero `any` types
+- βœ… Proper TypeScript compilation
+- βœ… ESLint validation passed
+- βœ… Test coverage (created where missing)
+
+---
+
+## πŸ“ˆ Success Metrics
+
+**Per-File Gates**:
+- βœ… `npm run type` passes after each migration
+- βœ… Zero `any` types introduced  
+- βœ… All imports properly typed
+- βœ… Types filed in correct hierarchy
+
+**Overall Progress**:
+- **Automatic Integration Rate**: 100% (3/3 migrations required no coordinator fixes)
+- **Test Coverage**: Improved (1 new test file created)
+- **Type Safety**: Enhanced with proper interfaces and generics
+
+---
+
+*This is a claudette-managed progressive refactor. All documentation and coordination resources are organized under `.claude/projects/js-to-ts/`*
diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.js b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.ts
similarity index 82%
rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.js
rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.ts
index 50bb0e1f52..6c430c4074 100644
--- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.js
+++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/roundDecimal.ts
@@ -17,11 +17,14 @@
  * under the License.
  */
 
-export default function roundDecimal(number, precision) {
-  let roundedNumber;
-  let p = precision;
+export default function roundDecimal(
+  number: number,
+  precision?: number,
+): number {
+  let roundedNumber: number;
   if (precision) {
-    roundedNumber = Math.round(number * (p = 10 ** p)) / p;
+    const p = 10 ** precision;
+    roundedNumber = Math.round(number * p) / p;
   } else {
     roundedNumber = Math.round(number);
   }
diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/test/utils/roundDecimal.test.js b/superset-frontend/plugins/legacy-plugin-chart-map-box/test/utils/roundDecimal.test.ts
similarity index 100%
rename from superset-frontend/plugins/legacy-plugin-chart-map-box/test/utils/roundDecimal.test.js
rename to superset-frontend/plugins/legacy-plugin-chart-map-box/test/utils/roundDecimal.test.ts
diff --git a/superset-frontend/spec/fixtures/mockDashboardLayout.js b/superset-frontend/spec/fixtures/mockDashboardLayout.js
index 404b415d78..85c82edcac 100644
--- a/superset-frontend/spec/fixtures/mockDashboardLayout.js
+++ b/superset-frontend/spec/fixtures/mockDashboardLayout.js
@@ -258,20 +258,36 @@ export const dashboardWithFilter = {
     type: DASHBOARD_ROOT_TYPE,
     id: DASHBOARD_ROOT_ID,
     children: [DASHBOARD_GRID_ID],
+    meta: {
+      chartId: 0,
+      height: 0,
+      uuid: '',
+      width: 0,
+    },
   },
 
   [DASHBOARD_GRID_ID]: {
     type: DASHBOARD_GRID_TYPE,
     id: DASHBOARD_GRID_ID,
     children: ['ROW_ID'],
-    meta: {},
+    meta: {
+      chartId: 0,
+      height: 0,
+      uuid: '',
+      width: 0,
+    },
   },
 
   [DASHBOARD_HEADER_ID]: {
     type: DASHBOARD_HEADER_TYPE,
     id: DASHBOARD_HEADER_ID,
+    children: [],
     meta: {
       text: 'New dashboard',
+      chartId: 0,
+      height: 0,
+      uuid: '',
+      width: 0,
     },
   },
 
@@ -285,6 +301,7 @@ export const dashboardWithFilter = {
     ...newComponentFactory(CHART_TYPE),
     id: 'FILTER_ID',
     meta: {
+      ...newComponentFactory(CHART_TYPE).meta,
       chartId: filterId,
       width: 3,
       height: 10,
@@ -296,6 +313,7 @@ export const dashboardWithFilter = {
     ...newComponentFactory(CHART_TYPE),
     id: 'CHART_ID',
     meta: {
+      ...newComponentFactory(CHART_TYPE).meta,
       chartId,
       width: 3,
       height: 10,
diff --git a/superset-frontend/src/components/MessageToasts/reducers.js b/superset-frontend/src/components/MessageToasts/reducers.ts
similarity index 77%
rename from superset-frontend/src/components/MessageToasts/reducers.js
rename to superset-frontend/src/components/MessageToasts/reducers.ts
index e999d12adb..9209164c57 100644
--- a/superset-frontend/src/components/MessageToasts/reducers.js
+++ b/superset-frontend/src/components/MessageToasts/reducers.ts
@@ -17,8 +17,26 @@
  * under the License.
  */
 import { ADD_TOAST, REMOVE_TOAST } from './actions';
+import { ToastMeta } from './types';
 
-export default function messageToastsReducer(toasts = [], action) {
+interface AddToastAction {
+  type: typeof ADD_TOAST;
+  payload: ToastMeta;
+}
+
+interface RemoveToastAction {
+  type: typeof REMOVE_TOAST;
+  payload: {
+    id: string;
+  };
+}
+
+type ToastAction = AddToastAction | RemoveToastAction;
+
+export default function messageToastsReducer(
+  toasts: ToastMeta[] = [],
+  action: ToastAction,
+): ToastMeta[] {
   switch (action.type) {
     case ADD_TOAST: {
       const { payload: toast } = action;
diff --git a/superset-frontend/src/components/Tag/Tag.stories.tsx b/superset-frontend/src/components/Tag/Tag.stories.tsx
index 57d2df7a43..36676a89ee 100644
--- a/superset-frontend/src/components/Tag/Tag.stories.tsx
+++ b/superset-frontend/src/components/Tag/Tag.stories.tsx
@@ -20,7 +20,7 @@ import { useState } from 'react';
 import { Meta, StoryObj } from '@storybook/react';
 import { Tag } from 'src/components/Tag';
 import type { CheckableTagProps } from 'src/components/Tag';
-import TagType from 'src/types/TagType';
+import type { TagType } from 'src/types/TagType';
 
 export default {
   title: 'Components/Tag',
diff --git a/superset-frontend/src/components/Tag/Tag.test.tsx b/superset-frontend/src/components/Tag/Tag.test.tsx
index b130432694..d4077118ee 100644
--- a/superset-frontend/src/components/Tag/Tag.test.tsx
+++ b/superset-frontend/src/components/Tag/Tag.test.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 import { render, screen } from 'spec/helpers/testing-library';
-import TagType from 'src/types/TagType';
+import type { TagType } from 'src/types/TagType';
 import { Tag } from '.';
 
 const mockedProps: TagType = {
diff --git a/superset-frontend/src/components/Tag/index.tsx b/superset-frontend/src/components/Tag/index.tsx
index 37cc02ec12..308a5ff6e0 100644
--- a/superset-frontend/src/components/Tag/index.tsx
+++ b/superset-frontend/src/components/Tag/index.tsx
@@ -19,7 +19,7 @@
 
 import { styled } from '@superset-ui/core';
 import { Link } from 'react-router-dom';
-import TagType from 'src/types/TagType';
+import type { TagType } from 'src/types/TagType';
 import { Tag as AntdTag } from '@superset-ui/core/components/Tag';
 import { Tooltip } from '@superset-ui/core/components/Tooltip';
 import type { TagProps } from 'antd/es';
diff --git a/superset-frontend/src/components/Tag/utils.tsx b/superset-frontend/src/components/Tag/utils.tsx
index ea3f9b1982..b275698868 100644
--- a/superset-frontend/src/components/Tag/utils.tsx
+++ b/superset-frontend/src/components/Tag/utils.tsx
@@ -23,7 +23,7 @@ import {
   SupersetClient,
   t,
 } from '@superset-ui/core';
-import Tag from 'src/types/TagType';
+import type { TagType } from 'src/types/TagType';
 
 import rison from 'rison';
 import { cacheWrapper } from 'src/utils/cacheWrapper';
@@ -43,7 +43,7 @@ type SelectTagsValue = {
 };
 
 export const tagToSelectOption = (
-  tag: Tag & { table_name: string },
+  tag: TagType & { table_name: string },
 ): SelectTagsValue => ({
   value: tag.id,
   label: tag.name,
diff --git a/superset-frontend/src/components/TagsList/index.tsx b/superset-frontend/src/components/TagsList/index.tsx
index 15b95ef440..a83861f26b 100644
--- a/superset-frontend/src/components/TagsList/index.tsx
+++ b/superset-frontend/src/components/TagsList/index.tsx
@@ -19,7 +19,7 @@
 
 import { useMemo, useState } from 'react';
 import { styled } from '@superset-ui/core';
-import TagType from 'src/types/TagType';
+import type { TagType } from 'src/types/TagType';
 import { Tag } from 'src/components/Tag';
 
 export type TagsListProps = {
diff --git a/superset-frontend/src/dashboard/actions/sliceEntities.ts b/superset-frontend/src/dashboard/actions/sliceEntities.ts
index 91b5faf6f0..de48b7f59e 100644
--- a/superset-frontend/src/dashboard/actions/sliceEntities.ts
+++ b/superset-frontend/src/dashboard/actions/sliceEntities.ts
@@ -26,9 +26,40 @@ import {
 import { addDangerToast } from 'src/components/MessageToasts/actions';
 import { Dispatch } from 'redux';
 import { Slice } from '../types';
+import { HYDRATE_DASHBOARD } from './hydrate';
 
 const FETCH_SLICES_PAGE_SIZE = 200;
 
+// State types
+export interface SliceEntitiesState {
+  slices: { [id: number]: Slice };
+  isLoading: boolean;
+  errorMessage: string | null;
+  lastUpdated: number;
+}
+
+// Action types
+export type SliceEntitiesActionPayload =
+  | {
+      type: typeof ADD_SLICES;
+      payload: { slices: { [id: number]: Slice } };
+    }
+  | {
+      type: typeof SET_SLICES;
+      payload: { slices: { [id: number]: Slice } };
+    }
+  | {
+      type: typeof FETCH_ALL_SLICES_STARTED;
+    }
+  | {
+      type: typeof FETCH_ALL_SLICES_FAILED;
+      payload: { error: string };
+    }
+  | {
+      type: typeof HYDRATE_DASHBOARD;
+      data: { sliceEntities: SliceEntitiesState };
+    };
+
 export function getDatasourceParameter(
   datasourceId: number,
   datasourceType: DatasourceType,
diff --git a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
index 6e98110537..c913424e6d 100644
--- a/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
+++ b/superset-frontend/src/dashboard/components/AddSliceCard/AddSliceCard.tsx
@@ -170,7 +170,9 @@ const SliceAddedBadge: FC<{ placeholder?: HTMLDivElement }> = ({
 const AddSliceCard: FC<{
   datasourceUrl?: string;
   datasourceName?: string;
-  innerRef?: RefObject<HTMLDivElement>;
+  innerRef?:
+    | RefObject<HTMLDivElement>
+    | ((node: HTMLDivElement | null) => void);
   isSelected?: boolean;
   lastModified?: string;
   sliceName: string;
diff --git a/superset-frontend/src/dashboard/components/SliceAdder.tsx b/superset-frontend/src/dashboard/components/SliceAdder.tsx
index dfde8ec7a8..82c4cae969 100644
--- a/superset-frontend/src/dashboard/components/SliceAdder.tsx
+++ b/superset-frontend/src/dashboard/components/SliceAdder.tsx
@@ -50,6 +50,7 @@ import { Dispatch } from 'redux';
 import { Slice } from 'src/dashboard/types';
 import { withTheme, Theme } from '@emotion/react';
 import { navigateTo } from 'src/utils/navigationUtils';
+import type { ConnectDragSource } from 'react-dnd';
 import AddSliceCard from './AddSliceCard';
 import AddSliceDragPreview from './dnd/AddSliceDragPreview';
 import { DragDroppable } from './dnd/DragDroppable';
@@ -312,7 +313,7 @@ class SliceAdder extends Component<SliceAdderProps, SliceAdderState> {
         // actual style should be applied to nested AddSliceCard component
         style={{}}
       >
-        {({ dragSourceRef }) => (
+        {({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
           <AddSliceCard
             innerRef={dragSourceRef}
             style={style}
diff --git a/superset-frontend/src/dashboard/components/dnd/dragDroppableConfig.js b/superset-frontend/src/dashboard/components/dnd/dragDroppableConfig.js
deleted file mode 100644
index bd8ab744b1..0000000000
--- a/superset-frontend/src/dashboard/components/dnd/dragDroppableConfig.js
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import handleHover from './handleHover';
-import handleDrop from './handleDrop';
-
-// note: the 'type' hook is not useful for us as dropping is contingent on other properties
-const TYPE = 'DRAG_DROPPABLE';
-
-export const dragConfig = [
-  TYPE,
-  {
-    canDrag(props) {
-      return !props.disableDragDrop;
-    },
-
-    // this defines the dragging item object returned by monitor.getItem()
-    beginDrag(props /* , monitor, component */) {
-      const { component, index, parentComponent = {} } = props;
-      return {
-        type: component.type,
-        id: component.id,
-        meta: component.meta,
-        index,
-        parentId: parentComponent.id,
-        parentType: parentComponent.type,
-      };
-    },
-  },
-  function dragStateToProps(connect, monitor) {
-    return {
-      dragSourceRef: connect.dragSource(),
-      dragPreviewRef: connect.dragPreview(),
-      isDragging: monitor.isDragging(),
-      dragComponentType: monitor.getItem()?.type,
-      dragComponentId: monitor.getItem()?.id,
-    };
-  },
-];
-
-export const dropConfig = [
-  TYPE,
-  {
-    canDrop(props) {
-      return !props.disableDragDrop;
-    },
-    hover(props, monitor, component) {
-      if (component && component.mounted) {
-        handleHover(props, monitor, component);
-      }
-    },
-    // note:
-    //  the react-dnd api requires that the drop() method return a result or undefined
-    //  monitor.didDrop() cannot be used because it returns true only for the most-nested target
-    drop(props, monitor, component) {
-      const dropResult = monitor.getDropResult();
-      if ((!dropResult || !dropResult.destination) && component.mounted) {
-        return handleDrop(props, monitor, component);
-      }
-      return undefined;
-    },
-  },
-  function dropStateToProps(connect, monitor) {
-    return {
-      droppableRef: connect.dropTarget(),
-      isDraggingOver: monitor.isOver(),
-      isDraggingOverShallow: monitor.isOver({ shallow: true }),
-    };
-  },
-];
diff --git a/superset-frontend/src/dashboard/components/dnd/dragDroppableConfig.ts b/superset-frontend/src/dashboard/components/dnd/dragDroppableConfig.ts
new file mode 100644
index 0000000000..84a90977c6
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/dnd/dragDroppableConfig.ts
@@ -0,0 +1,188 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  DragSourceMonitor,
+  DropTargetMonitor,
+  ConnectDragSource,
+  ConnectDragPreview,
+  ConnectDropTarget,
+} from 'react-dnd';
+import { LayoutItem, ComponentType } from 'src/dashboard/types';
+import handleHover from './handleHover';
+import handleDrop from './handleDrop';
+
+// note: the 'type' hook is not useful for us as dropping is contingent on other properties
+const TYPE = 'DRAG_DROPPABLE';
+
+export interface DragDroppableProps {
+  component: LayoutItem;
+  parentComponent?: LayoutItem;
+  index: number;
+  disableDragDrop: boolean;
+  onDrop?: (dropResult: DropResult) => void;
+  onHover?: () => void;
+  dropToChild?: boolean | ((draggingItem: DragItem) => boolean);
+}
+
+export interface DragItem {
+  type: ComponentType;
+  id: string;
+  meta: LayoutItem['meta'];
+  index: number;
+  parentId?: string;
+  parentType?: ComponentType;
+}
+
+export interface DropResult {
+  source: {
+    id: string;
+    type: ComponentType;
+    index: number;
+  };
+  dragging: {
+    id: string;
+    type: ComponentType;
+    meta: LayoutItem['meta'];
+  };
+  destination?: {
+    id: string;
+    type: ComponentType;
+    index: number;
+  };
+  position?: string;
+}
+
+export interface DragStateProps {
+  dragSourceRef: ConnectDragSource;
+  dragPreviewRef: ConnectDragPreview;
+  isDragging: boolean;
+  dragComponentType?: ComponentType;
+  dragComponentId?: string;
+}
+
+export interface DropStateProps {
+  droppableRef: ConnectDropTarget;
+  isDraggingOver: boolean;
+  isDraggingOverShallow: boolean;
+}
+
+export interface DragDroppableComponent {
+  mounted: boolean;
+  props: DragDroppableProps;
+  setState: (stateUpdate: () => { dropIndicator: string | null }) => void;
+}
+
+export const dragConfig: [
+  string,
+  {
+    canDrag: (props: DragDroppableProps) => boolean;
+    beginDrag: (props: DragDroppableProps) => DragItem;
+  },
+  (connect: any, monitor: DragSourceMonitor) => DragStateProps,
+] = [
+  TYPE,
+  {
+    canDrag(props: DragDroppableProps): boolean {
+      return !props.disableDragDrop;
+    },
+
+    // this defines the dragging item object returned by monitor.getItem()
+    beginDrag(props: DragDroppableProps): DragItem {
+      const { component, index, parentComponent } = props;
+      return {
+        type: component.type,
+        id: component.id,
+        meta: component.meta,
+        index,
+        parentId: parentComponent?.id,
+        parentType: parentComponent?.type,
+      };
+    },
+  },
+  function dragStateToProps(
+    connect: any,
+    monitor: DragSourceMonitor,
+  ): DragStateProps {
+    return {
+      dragSourceRef: connect.dragSource(),
+      dragPreviewRef: connect.dragPreview(),
+      isDragging: monitor.isDragging(),
+      dragComponentType: monitor.getItem()?.type as ComponentType,
+      dragComponentId: monitor.getItem()?.id as string,
+    };
+  },
+];
+
+export const dropConfig: [
+  string,
+  {
+    canDrop: (props: DragDroppableProps) => boolean;
+    hover: (
+      props: DragDroppableProps,
+      monitor: DropTargetMonitor,
+      component: DragDroppableComponent,
+    ) => void;
+    drop: (
+      props: DragDroppableProps,
+      monitor: DropTargetMonitor,
+      component: DragDroppableComponent,
+    ) => DropResult | undefined;
+  },
+  (connect: any, monitor: DropTargetMonitor) => DropStateProps,
+] = [
+  TYPE,
+  {
+    canDrop(props: DragDroppableProps): boolean {
+      return !props.disableDragDrop;
+    },
+    hover(
+      props: DragDroppableProps,
+      monitor: DropTargetMonitor,
+      component: DragDroppableComponent,
+    ): void {
+      if (component && component.mounted) {
+        handleHover(props, monitor, component);
+      }
+    },
+    // note:
+    //  the react-dnd api requires that the drop() method return a result or undefined
+    //  monitor.didDrop() cannot be used because it returns true only for the most-nested target
+    drop(
+      props: DragDroppableProps,
+      monitor: DropTargetMonitor,
+      component: DragDroppableComponent,
+    ): DropResult | undefined {
+      const dropResult = monitor.getDropResult() as DropResult | null;
+      if ((!dropResult || !dropResult.destination) && component.mounted) {
+        return handleDrop(props, monitor, component);
+      }
+      return undefined;
+    },
+  },
+  function dropStateToProps(
+    connect: any,
+    monitor: DropTargetMonitor,
+  ): DropStateProps {
+    return {
+      droppableRef: connect.dropTarget(),
+      isDraggingOver: monitor.isOver(),
+      isDraggingOverShallow: monitor.isOver({ shallow: true }),
+    };
+  },
+];
diff --git a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx
index eb06833fba..df50ed37b3 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/DynamicComponent/DynamicComponent.tsx
@@ -22,6 +22,7 @@ import backgroundStyleOptions from 'src/dashboard/util/backgroundStyleOptions';
 import cx from 'classnames';
 import { shallowEqual, useSelector } from 'react-redux';
 import { ResizeCallback, ResizeStartCallback } from 're-resizable';
+import type { ConnectDragSource } from 'react-dnd';
 import { Draggable } from '../../dnd/DragDroppable';
 import { COLUMN_TYPE, ROW_TYPE } from '../../../util/componentTypes';
 import WithPopoverMenu from '../../menu/WithPopoverMenu';
@@ -119,7 +120,7 @@ const DynamicComponent: FC<DynamicComponentProps> = ({
       onDrop={handleComponentDrop}
       editMode={editMode}
     >
-      {({ dragSourceRef }) => (
+      {({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
         <WithPopoverMenu
           menuItems={[
             <BackgroundStyleDropdown
diff --git a/superset-frontend/src/dashboard/components/gridComponents/new/DraggableNewComponent.tsx b/superset-frontend/src/dashboard/components/gridComponents/new/DraggableNewComponent.tsx
index 3733bfdead..3ceda79442 100644
--- a/superset-frontend/src/dashboard/components/gridComponents/new/DraggableNewComponent.tsx
+++ b/superset-frontend/src/dashboard/components/gridComponents/new/DraggableNewComponent.tsx
@@ -20,6 +20,7 @@ import { PureComponent } from 'react';
 import cx from 'classnames';
 import { css, styled } from '@superset-ui/core';
 import { DragDroppable } from 'src/dashboard/components/dnd/DragDroppable';
+import type { ConnectDragSource } from 'react-dnd';
 import { NEW_COMPONENTS_SOURCE_ID } from 'src/dashboard/util/constants';
 import { NEW_COMPONENT_SOURCE_TYPE } from 'src/dashboard/util/componentTypes';
 
@@ -81,7 +82,7 @@ export default class DraggableNewComponent extends PureComponent<DraggableNewCom
         depth={0}
         editMode
       >
-        {({ dragSourceRef }) => (
+        {({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
           <NewComponent ref={dragSourceRef} data-test="new-component">
             <NewComponentPlaceholder
               className={cx('new-component-placeholder', className)}
diff --git a/superset-frontend/src/dashboard/fixtures/emptyDashboardLayout.js b/superset-frontend/src/dashboard/fixtures/emptyDashboardLayout.ts
similarity index 67%
rename from superset-frontend/src/dashboard/fixtures/emptyDashboardLayout.js
rename to superset-frontend/src/dashboard/fixtures/emptyDashboardLayout.ts
index d693aca878..55279e6eef 100644
--- a/superset-frontend/src/dashboard/fixtures/emptyDashboardLayout.js
+++ b/superset-frontend/src/dashboard/fixtures/emptyDashboardLayout.ts
@@ -28,11 +28,38 @@ import {
   DASHBOARD_GRID_ID,
 } from '../util/constants';
 
-export default {
+import type { DashboardLayout, LayoutItemMeta } from '../types';
+
+// Create minimal meta objects that satisfy the LayoutItemMeta type requirements
+const rootMeta: LayoutItemMeta = {
+  chartId: 0,
+  height: 0,
+  uuid: '',
+  width: 0,
+};
+
+const gridMeta: LayoutItemMeta = {
+  chartId: 0,
+  height: 0,
+  uuid: '',
+  width: 0,
+};
+
+const headerMeta: LayoutItemMeta = {
+  chartId: 0,
+  height: 0,
+  uuid: '',
+  width: 0,
+  text: 'New dashboard',
+};
+
+const emptyDashboardLayout: DashboardLayout = {
   [DASHBOARD_ROOT_ID]: {
     type: DASHBOARD_ROOT_TYPE,
     id: DASHBOARD_ROOT_ID,
     children: [DASHBOARD_GRID_ID],
+    parents: [],
+    meta: rootMeta,
   },
 
   [DASHBOARD_GRID_ID]: {
@@ -40,14 +67,16 @@ export default {
     id: DASHBOARD_GRID_ID,
     children: [],
     parents: [DASHBOARD_ROOT_ID],
-    meta: {},
+    meta: gridMeta,
   },
 
   [DASHBOARD_HEADER_ID]: {
     type: HEADER_TYPE,
     id: DASHBOARD_HEADER_ID,
-    meta: {
-      text: 'New dashboard',
-    },
+    children: [],
+    parents: [],
+    meta: headerMeta,
   },
 };
+
+export default emptyDashboardLayout;
diff --git a/superset-frontend/src/dashboard/reducers/sliceEntities.js b/superset-frontend/src/dashboard/reducers/sliceEntities.ts
similarity index 80%
rename from superset-frontend/src/dashboard/reducers/sliceEntities.js
rename to superset-frontend/src/dashboard/reducers/sliceEntities.ts
index 1a065b11fa..eb90138f54 100644
--- a/superset-frontend/src/dashboard/reducers/sliceEntities.js
+++ b/superset-frontend/src/dashboard/reducers/sliceEntities.ts
@@ -23,10 +23,12 @@ import {
   FETCH_ALL_SLICES_STARTED,
   ADD_SLICES,
   SET_SLICES,
+  SliceEntitiesState,
+  SliceEntitiesActionPayload,
 } from '../actions/sliceEntities';
 import { HYDRATE_DASHBOARD } from '../actions/hydrate';
 
-export const initSliceEntities = {
+export const initSliceEntities: SliceEntitiesState = {
   slices: {},
   isLoading: true,
   errorMessage: null,
@@ -34,37 +36,34 @@ export const initSliceEntities = {
 };
 
 export default function sliceEntitiesReducer(
-  state = initSliceEntities,
-  action,
-) {
-  const actionHandlers = {
-    [HYDRATE_DASHBOARD]() {
+  state: SliceEntitiesState = initSliceEntities,
+  action: SliceEntitiesActionPayload,
+): SliceEntitiesState {
+  switch (action.type) {
+    case HYDRATE_DASHBOARD:
       return {
         ...action.data.sliceEntities,
       };
-    },
-    [FETCH_ALL_SLICES_STARTED]() {
+    case FETCH_ALL_SLICES_STARTED:
       return {
         ...state,
         isLoading: true,
       };
-    },
-    [ADD_SLICES]() {
+    case ADD_SLICES:
       return {
         ...state,
         isLoading: false,
         slices: { ...state.slices, ...action.payload.slices },
         lastUpdated: new Date().getTime(),
       };
-    },
-    [SET_SLICES]() {
+    case SET_SLICES:
       return {
+        ...state,
         isLoading: false,
         slices: { ...action.payload.slices },
         lastUpdated: new Date().getTime(),
       };
-    },
-    [FETCH_ALL_SLICES_FAILED]() {
+    case FETCH_ALL_SLICES_FAILED:
       return {
         ...state,
         isLoading: false,
@@ -72,11 +71,7 @@ export default function sliceEntitiesReducer(
         errorMessage:
           action.payload.error || t('Could not fetch all saved charts'),
       };
-    },
-  };
-
-  if (action.type in actionHandlers) {
-    return actionHandlers[action.type]();
+    default:
+      return state;
   }
-  return state;
 }
diff --git a/superset-frontend/src/dashboard/util/activeDashboardFilters.js b/superset-frontend/src/dashboard/util/activeDashboardFilters.js
deleted file mode 100644
index ef6fd7f5e1..0000000000
--- a/superset-frontend/src/dashboard/util/activeDashboardFilters.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import { isEmpty } from 'lodash';
-import { mapValues, flow, keyBy } from 'lodash/fp';
-import {
-  getChartIdAndColumnFromFilterKey,
-  getDashboardFilterKey,
-} from './getDashboardFilterKey';
-import { CHART_TYPE } from './componentTypes';
-import { DASHBOARD_FILTER_SCOPE_GLOBAL } from '../reducers/dashboardFilters';
-
-let activeFilters = {};
-let appliedFilterValuesByChart = {};
-let allComponents = {};
-
-// output: { [id_column]: { values, scope } }
-export function getActiveFilters() {
-  return activeFilters;
-}
-
-// this function is to find all filter values applied to a chart,
-// it goes through all active filters and their scopes.
-// return: { [column]: array of selected values }
-export function getAppliedFilterValues(chartId, filters) {
-  // use cached data if possible
-  if (!(chartId in appliedFilterValuesByChart)) {
-    const applicableFilters = Object.entries(filters || activeFilters).filter(
-      ([, { scope: chartIds }]) => chartIds.includes(chartId),
-    );
-    appliedFilterValuesByChart[chartId] = flow(
-      keyBy(
-        ([filterKey]) => getChartIdAndColumnFromFilterKey(filterKey).column,
-      ),
-      mapValues(([, { values }]) => values),
-    )(applicableFilters);
-  }
-  return appliedFilterValuesByChart[chartId];
-}
-
-/**
- * @deprecated Please use src/dashboard/util/getChartIdsInFilterScope instead
- */
-export function getChartIdsInFilterScope({ filterScope }) {
-  function traverse(chartIds = [], component = {}, immuneChartIds = []) {
-    if (!component) {
-      return;
-    }
-
-    if (
-      component.type === CHART_TYPE &&
-      component.meta &&
-      component.meta.chartId &&
-      !immuneChartIds.includes(component.meta.chartId)
-    ) {
-      chartIds.push(component.meta.chartId);
-    } else if (component.children) {
-      component.children.forEach(child =>
-        traverse(chartIds, allComponents[child], immuneChartIds),
-      );
-    }
-  }
-
-  const chartIds = [];
-  const { scope: scopeComponentIds, immune: immuneChartIds } =
-    filterScope || DASHBOARD_FILTER_SCOPE_GLOBAL;
-  scopeComponentIds.forEach(componentId =>
-    traverse(chartIds, allComponents[componentId], immuneChartIds),
-  );
-
-  return chartIds;
-}
-
-// non-empty filter fields in dashboardFilters,
-// activeFilters map contains selected values and filter scope.
-// values: array of selected values
-// scope: array of chartIds that applicable to the filter field.
-export function buildActiveFilters({ dashboardFilters = {}, components = {} }) {
-  // clear cache
-  if (!isEmpty(components)) {
-    allComponents = components;
-  }
-  appliedFilterValuesByChart = {};
-  activeFilters = Object.values(dashboardFilters).reduce((result, filter) => {
-    const { chartId, columns, scopes } = filter;
-    const nonEmptyFilters = {};
-
-    Object.keys(columns).forEach(column => {
-      if (
-        Array.isArray(columns[column])
-          ? columns[column].length
-          : columns[column] !== undefined
-      ) {
-        // remove filter itself
-        const scope = getChartIdsInFilterScope({
-          filterScope: scopes[column],
-        }).filter(id => chartId !== id);
-
-        nonEmptyFilters[getDashboardFilterKey({ chartId, column })] = {
-          values: columns[column],
-          scope,
-        };
-      }
-    });
-
-    return {
-      ...result,
-      ...nonEmptyFilters,
-    };
-  }, {});
-}
diff --git a/superset-frontend/src/dashboard/util/activeDashboardFilters.ts b/superset-frontend/src/dashboard/util/activeDashboardFilters.ts
new file mode 100644
index 0000000000..31823ff6f8
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/activeDashboardFilters.ts
@@ -0,0 +1,211 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { isEmpty } from 'lodash';
+import { mapValues, flow, keyBy } from 'lodash/fp';
+import {
+  JsonValue,
+  DataRecordFilters,
+  DataRecordValue,
+} from '@superset-ui/core';
+import {
+  getChartIdAndColumnFromFilterKey,
+  getDashboardFilterKey,
+} from './getDashboardFilterKey';
+import { CHART_TYPE } from './componentTypes';
+import { DASHBOARD_FILTER_SCOPE_GLOBAL } from '../reducers/dashboardFilters';
+import { LayoutItem } from '../types';
+
+// Type definitions for filters
+interface FilterScope {
+  scope: string[];
+  immune?: number[];
+}
+
+interface DashboardFilterColumn {
+  [column: string]: JsonValue[] | JsonValue;
+}
+
+interface DashboardFilterScopes {
+  [column: string]: FilterScope;
+}
+
+interface DashboardFilter {
+  chartId: number;
+  columns: DashboardFilterColumn;
+  scopes: DashboardFilterScopes;
+}
+
+interface DashboardFilters {
+  [filterId: string]: DashboardFilter;
+}
+
+interface Components {
+  [componentId: string]: LayoutItem;
+}
+
+interface ActiveFilter {
+  values: JsonValue[] | JsonValue;
+  scope: number[];
+}
+
+interface ActiveFilters {
+  [filterKey: string]: ActiveFilter;
+}
+
+interface AppliedFilterValuesByChart {
+  [chartId: number]: DataRecordFilters;
+}
+
+interface GetChartIdsInFilterScopeProps {
+  filterScope?: FilterScope;
+}
+
+interface BuildActiveFiltersProps {
+  dashboardFilters?: DashboardFilters;
+  components?: Components;
+}
+
+let activeFilters: ActiveFilters = {};
+let appliedFilterValuesByChart: AppliedFilterValuesByChart = {};
+let allComponents: Components = {};
+
+// output: { [id_column]: { values, scope } }
+export function getActiveFilters(): ActiveFilters {
+  return activeFilters;
+}
+
+// this function is to find all filter values applied to a chart,
+// it goes through all active filters and their scopes.
+// return: { [column]: array of selected values }
+export function getAppliedFilterValues(
+  chartId: number,
+  filters?: ActiveFilters,
+): DataRecordFilters {
+  // use cached data if possible
+  if (!(chartId in appliedFilterValuesByChart)) {
+    const applicableFilters = Object.entries(filters || activeFilters).filter(
+      ([, { scope: chartIds }]) => chartIds.includes(chartId),
+    );
+    appliedFilterValuesByChart[chartId] = flow(
+      keyBy(
+        ([filterKey]: [string, ActiveFilter]) =>
+          getChartIdAndColumnFromFilterKey(filterKey).column,
+      ),
+      mapValues(([, { values }]: [string, ActiveFilter]) => {
+        // Ensure values is always an array of DataRecordValue
+        if (Array.isArray(values)) {
+          return values.filter(
+            val => val !== null && val !== undefined,
+          ) as DataRecordValue[];
+        }
+        // If single value, wrap in array and filter valid values
+        return values !== null && values !== undefined
+          ? [values as DataRecordValue]
+          : [];
+      }),
+    )(applicableFilters);
+  }
+  return appliedFilterValuesByChart[chartId];
+}
+
+/**
+ * @deprecated Please use src/dashboard/util/getChartIdsInFilterScope instead
+ */
+export function getChartIdsInFilterScope({
+  filterScope,
+}: GetChartIdsInFilterScopeProps): number[] {
+  function traverse(
+    chartIds: number[] = [],
+    component: LayoutItem | undefined = undefined,
+    immuneChartIds: number[] = [],
+  ): void {
+    if (!component) {
+      return;
+    }
+
+    if (
+      component.type === CHART_TYPE &&
+      component.meta &&
+      component.meta.chartId &&
+      !immuneChartIds.includes(component.meta.chartId)
+    ) {
+      chartIds.push(component.meta.chartId);
+    } else if (component.children) {
+      component.children.forEach(child =>
+        traverse(chartIds, allComponents[child], immuneChartIds),
+      );
+    }
+  }
+
+  const chartIds: number[] = [];
+  const { scope: scopeComponentIds, immune: immuneChartIds = [] } =
+    filterScope || DASHBOARD_FILTER_SCOPE_GLOBAL;
+  scopeComponentIds.forEach(componentId =>
+    traverse(chartIds, allComponents[componentId], immuneChartIds),
+  );
+
+  return chartIds;
+}
+
+// non-empty filter fields in dashboardFilters,
+// activeFilters map contains selected values and filter scope.
+// values: array of selected values
+// scope: array of chartIds that applicable to the filter field.
+export function buildActiveFilters({
+  dashboardFilters = {},
+  components = {},
+}: BuildActiveFiltersProps): void {
+  // clear cache
+  if (!isEmpty(components)) {
+    allComponents = components;
+  }
+  appliedFilterValuesByChart = {};
+  activeFilters = Object.values(dashboardFilters).reduce(
+    (result: ActiveFilters, filter: DashboardFilter) => {
+      const { chartId, columns, scopes } = filter;
+      const nonEmptyFilters: ActiveFilters = {};
+
+      Object.keys(columns).forEach(column => {
+        if (
+          Array.isArray(columns[column])
+            ? (columns[column] as JsonValue[]).length
+            : columns[column] !== undefined
+        ) {
+          // remove filter itself
+          const scope = getChartIdsInFilterScope({
+            filterScope: scopes[column],
+          }).filter(id => chartId !== id);
+
+          nonEmptyFilters[
+            getDashboardFilterKey({ chartId: String(chartId), column })
+          ] = {
+            values: columns[column],
+            scope,
+          };
+        }
+      });
+
+      return {
+        ...result,
+        ...nonEmptyFilters,
+      };
+    },
+    {},
+  );
+}
diff --git a/superset-frontend/src/dashboard/util/buildFilterScopeTreeEntry.js b/superset-frontend/src/dashboard/util/buildFilterScopeTreeEntry.ts
similarity index 73%
rename from superset-frontend/src/dashboard/util/buildFilterScopeTreeEntry.js
rename to superset-frontend/src/dashboard/util/buildFilterScopeTreeEntry.ts
index e91ac51fbd..e1a5d74172 100644
--- a/superset-frontend/src/dashboard/util/buildFilterScopeTreeEntry.js
+++ b/superset-frontend/src/dashboard/util/buildFilterScopeTreeEntry.ts
@@ -16,17 +16,41 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { DashboardLayout } from '../types';
 import getFilterScopeNodesTree from './getFilterScopeNodesTree';
 import getFilterScopeParentNodes from './getFilterScopeParentNodes';
 import getKeyForFilterScopeTree from './getKeyForFilterScopeTree';
 import getSelectedChartIdForFilterScopeTree from './getSelectedChartIdForFilterScopeTree';
 
+interface FilterScopeMapItem {
+  checked?: number[];
+  expanded?: string[];
+}
+
+interface FilterScopeMap {
+  [key: string]: FilterScopeMapItem;
+}
+
+interface FilterScopeTreeEntry {
+  nodes: any[];
+  nodesFiltered: any[];
+  checked: string[];
+  expanded: string[];
+}
+
+interface BuildFilterScopeTreeEntryProps {
+  checkedFilterFields?: string[];
+  activeFilterField?: string;
+  filterScopeMap?: FilterScopeMap;
+  layout?: DashboardLayout;
+}
+
 export default function buildFilterScopeTreeEntry({
   checkedFilterFields = [],
   activeFilterField,
   filterScopeMap = {},
   layout = {},
-}) {
+}: BuildFilterScopeTreeEntryProps): Record<string, FilterScopeTreeEntry> {
   const key = getKeyForFilterScopeTree({
     checkedFilterFields,
     activeFilterField,
@@ -43,15 +67,15 @@ export default function buildFilterScopeTreeEntry({
     filterFields: editingList,
     selectedChartId,
   });
-  const checkedChartIdSet = new Set();
+  const checkedChartIdSet = new Set<string>();
   editingList.forEach(filterField => {
-    (filterScopeMap[filterField].checked || []).forEach(chartId => {
+    (filterScopeMap[filterField]?.checked || []).forEach(chartId => {
       checkedChartIdSet.add(`${chartId}:${filterField}`);
     });
   });
   const checked = [...checkedChartIdSet];
   const expanded = filterScopeMap[key]
-    ? filterScopeMap[key].expanded
+    ? filterScopeMap[key].expanded || []
     : getFilterScopeParentNodes(nodes, 1);
 
   return {
diff --git a/superset-frontend/src/dashboard/util/dropOverflowsParent.test.js b/superset-frontend/src/dashboard/util/dropOverflowsParent.test.ts
similarity index 82%
rename from superset-frontend/src/dashboard/util/dropOverflowsParent.test.js
rename to superset-frontend/src/dashboard/util/dropOverflowsParent.test.ts
index a790ee0c6e..32528daca2 100644
--- a/superset-frontend/src/dashboard/util/dropOverflowsParent.test.js
+++ b/superset-frontend/src/dashboard/util/dropOverflowsParent.test.ts
@@ -16,7 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import dropOverflowsParent from 'src/dashboard/util/dropOverflowsParent';
+// Layout type not directly used in tests - using object shapes for test data
+import dropOverflowsParent, {
+  type DropResult,
+} from 'src/dashboard/util/dropOverflowsParent';
 import { NEW_COMPONENTS_SOURCE_ID } from 'src/dashboard/util/constants';
 import {
   CHART_TYPE,
@@ -28,7 +31,7 @@ import {
 
 describe('dropOverflowsParent', () => {
   it('returns true if a parent does NOT have adequate width for child', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: '_' },
       destination: { id: 'a' },
       dragging: { id: 'z' },
@@ -56,11 +59,11 @@ describe('dropOverflowsParent', () => {
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(true);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(true);
   });
 
   it('returns false if a parent DOES have adequate width for child', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: '_' },
       destination: { id: 'a' },
       dragging: { id: 'z' },
@@ -88,11 +91,11 @@ describe('dropOverflowsParent', () => {
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(false);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(false);
   });
 
   it('returns false if a child CAN shrink to available parent space', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: '_' },
       destination: { id: 'a' },
       dragging: { id: 'z' },
@@ -120,11 +123,11 @@ describe('dropOverflowsParent', () => {
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(false);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(false);
   });
 
   it('returns true if a child CANNOT shrink to available parent space', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: '_' },
       destination: { id: 'a' },
       dragging: { id: 'b' },
@@ -153,11 +156,11 @@ describe('dropOverflowsParent', () => {
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(true);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(true);
   });
 
   it('returns true if a column has children that CANNOT shrink to available parent space', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: '_' },
       destination: { id: 'destination' },
       dragging: { id: 'dragging' },
@@ -191,18 +194,18 @@ describe('dropOverflowsParent', () => {
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(true);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(true);
     // remove children
     expect(
       dropOverflowsParent(dropResult, {
         ...layout,
-        dragging: { ...layout.dragging, children: [] },
-      }),
+        dragging: { ...layout.dragging, children: [] } as any,
+      } as any),
     ).toBe(false);
   });
 
   it('should work with new components that are not in the layout', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: NEW_COMPONENTS_SOURCE_ID },
       destination: { id: 'a' },
       dragging: { type: CHART_TYPE },
@@ -212,15 +215,15 @@ describe('dropOverflowsParent', () => {
       a: {
         id: 'a',
         type: ROW_TYPE,
-        children: [],
+        children: [] as any,
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(false);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(false);
   });
 
   it('source/destination without widths should not overflow parent', () => {
-    const dropResult = {
+    const dropResult: DropResult = {
       source: { id: '_' },
       destination: { id: 'tab' },
       dragging: { id: 'header' },
@@ -237,6 +240,6 @@ describe('dropOverflowsParent', () => {
       },
     };
 
-    expect(dropOverflowsParent(dropResult, layout)).toBe(false);
+    expect(dropOverflowsParent(dropResult, layout as any)).toBe(false);
   });
 });
diff --git a/superset-frontend/src/dashboard/util/dropOverflowsParent.js b/superset-frontend/src/dashboard/util/dropOverflowsParent.ts
similarity index 75%
rename from superset-frontend/src/dashboard/util/dropOverflowsParent.js
rename to superset-frontend/src/dashboard/util/dropOverflowsParent.ts
index 00cde762a4..4b0aae990b 100644
--- a/superset-frontend/src/dashboard/util/dropOverflowsParent.js
+++ b/superset-frontend/src/dashboard/util/dropOverflowsParent.ts
@@ -16,9 +16,22 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { ComponentType, Layout } from 'src/dashboard/types';
 import getComponentWidthFromDrop from './getComponentWidthFromDrop';
 
-export default function doesChildOverflowParent(dropResult, layout) {
+export interface DropResult {
+  source: { id: string };
+  destination: { id: string };
+  dragging: {
+    id?: string;
+    type?: ComponentType;
+  };
+}
+
+export default function doesChildOverflowParent(
+  dropResult: DropResult,
+  layout: Layout,
+): boolean {
   const childWidth = getComponentWidthFromDrop({ dropResult, layout });
   return typeof childWidth === 'number' && childWidth < 0;
 }
diff --git a/superset-frontend/src/dashboard/util/findFirstParentContainer.js b/superset-frontend/src/dashboard/util/findFirstParentContainer.ts
similarity index 89%
rename from superset-frontend/src/dashboard/util/findFirstParentContainer.js
rename to superset-frontend/src/dashboard/util/findFirstParentContainer.ts
index 217504bdf4..7169ee2502 100644
--- a/superset-frontend/src/dashboard/util/findFirstParentContainer.js
+++ b/superset-frontend/src/dashboard/util/findFirstParentContainer.ts
@@ -18,8 +18,11 @@
  */
 import { TABS_TYPE } from './componentTypes';
 import { DASHBOARD_ROOT_ID } from './constants';
+import { DashboardLayout } from '../types';
 
-export default function findFirstParentContainerId(layout = {}) {
+export default function findFirstParentContainerId(
+  layout: DashboardLayout = {},
+): string {
   // DASHBOARD_GRID_TYPE or TABS_TYPE?
   let parent = layout[DASHBOARD_ROOT_ID];
   if (
diff --git a/superset-frontend/src/dashboard/util/getChartIdsFromLayout.test.js b/superset-frontend/src/dashboard/util/getChartIdsFromLayout.test.ts
similarity index 70%
rename from superset-frontend/src/dashboard/util/getChartIdsFromLayout.test.js
rename to superset-frontend/src/dashboard/util/getChartIdsFromLayout.test.ts
index a008b5a267..f90e335252 100644
--- a/superset-frontend/src/dashboard/util/getChartIdsFromLayout.test.js
+++ b/superset-frontend/src/dashboard/util/getChartIdsFromLayout.test.ts
@@ -18,36 +18,55 @@
  */
 import getChartIdsFromLayout from 'src/dashboard/util/getChartIdsFromLayout';
 import { ROW_TYPE, CHART_TYPE } from 'src/dashboard/util/componentTypes';
+import type { DashboardLayout } from '../types';
 
 describe('getChartIdsFromLayout', () => {
-  const mockLayout = {
+  const mockLayout: DashboardLayout = {
     a: {
       id: 'a',
       type: CHART_TYPE,
-      meta: { chartId: 'A' },
+      children: [],
+      meta: {
+        chartId: 123,
+        height: 400,
+        width: 400,
+        uuid: 'uuid-a',
+      },
     },
     b: {
       id: 'b',
       type: CHART_TYPE,
-      meta: { chartId: 'B' },
+      children: [],
+      meta: {
+        chartId: 456,
+        height: 400,
+        width: 400,
+        uuid: 'uuid-b',
+      },
     },
     c: {
       id: 'c',
       type: ROW_TYPE,
-      meta: { chartId: 'C' },
+      children: [],
+      meta: {
+        chartId: 789,
+        height: 400,
+        width: 400,
+        uuid: 'uuid-c',
+      },
     },
   };
 
   it('should return an array of chartIds', () => {
     const result = getChartIdsFromLayout(mockLayout);
     expect(Array.isArray(result)).toBe(true);
-    expect(result.includes('A')).toBe(true);
-    expect(result.includes('B')).toBe(true);
+    expect(result.includes(123)).toBe(true);
+    expect(result.includes(456)).toBe(true);
   });
 
   it('should return ids only from CHART_TYPE components', () => {
     const result = getChartIdsFromLayout(mockLayout);
     expect(result).toHaveLength(2);
-    expect(result.includes('C')).toBe(false);
+    expect(result.includes(789)).toBe(false);
   });
 });
diff --git a/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.js b/superset-frontend/src/dashboard/util/getChartIdsFromLayout.ts
similarity index 63%
copy from superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.js
copy to superset-frontend/src/dashboard/util/getChartIdsFromLayout.ts
index 42d317adaf..00e1992c4f 100644
--- a/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.js
+++ b/superset-frontend/src/dashboard/util/getChartIdsFromLayout.ts
@@ -16,15 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-/* eslint-disable no-param-reassign */
 import { CHART_TYPE } from './componentTypes';
+import type { DashboardLayout } from '../types';
 
-export default function getLayoutComponentFromChartId(layout, chartId) {
-  return Object.values(layout).find(
-    currentComponent =>
-      currentComponent &&
-      currentComponent.type === CHART_TYPE &&
-      currentComponent.meta &&
-      currentComponent.meta.chartId === chartId,
+export default function getChartIdsFromLayout(
+  layout: DashboardLayout,
+): number[] {
+  return Object.values(layout).reduce(
+    (chartIds: number[], currentComponent) => {
+      if (
+        currentComponent &&
+        currentComponent.type === CHART_TYPE &&
+        currentComponent.meta &&
+        currentComponent.meta.chartId
+      ) {
+        chartIds.push(currentComponent.meta.chartId);
+      }
+      return chartIds;
+    },
+    [],
   );
 }
diff --git a/superset-frontend/src/dashboard/util/serializeFilterScopes.js b/superset-frontend/src/dashboard/util/getDirectPathToTabIndex.test.ts
similarity index 56%
copy from superset-frontend/src/dashboard/util/serializeFilterScopes.js
copy to superset-frontend/src/dashboard/util/getDirectPathToTabIndex.test.ts
index 303171f24f..c9a0c769cd 100644
--- a/superset-frontend/src/dashboard/util/serializeFilterScopes.js
+++ b/superset-frontend/src/dashboard/util/getDirectPathToTabIndex.test.ts
@@ -16,19 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export default function serializeFilterScopes(dashboardFilters) {
-  return Object.values(dashboardFilters).reduce((map, { chartId, scopes }) => {
-    const scopesById = Object.keys(scopes).reduce(
-      (scopesByColumn, column) => ({
-        ...scopesByColumn,
-        [column]: scopes[column],
-      }),
-      {},
-    );
+import getDirectPathToTabIndex from './getDirectPathToTabIndex';
 
-    return {
-      ...map,
-      [chartId]: scopesById,
+describe('getDirectPathToTabIndex', () => {
+  it('builds path using parents, id, and child at index', () => {
+    const tabs = {
+      id: 'TABS_ID',
+      parents: ['ROOT', 'ROW_1'],
+      children: ['TAB_A', 'TAB_B', 'TAB_C'],
     };
-  }, {});
-}
+    expect(getDirectPathToTabIndex(tabs, 1)).toEqual([
+      'ROOT',
+      'ROW_1',
+      'TABS_ID',
+      'TAB_B',
+    ]);
+  });
+
+  it('handles missing parents', () => {
+    const tabs = {
+      id: 'TABS_ID',
+      children: ['TAB_A'],
+    };
+    expect(getDirectPathToTabIndex(tabs, 0)).toEqual(['TABS_ID', 'TAB_A']);
+  });
+});
diff --git a/superset-frontend/src/dashboard/util/getDirectPathToTabIndex.js b/superset-frontend/src/dashboard/util/getDirectPathToTabIndex.ts
similarity index 80%
rename from superset-frontend/src/dashboard/util/getDirectPathToTabIndex.js
rename to superset-frontend/src/dashboard/util/getDirectPathToTabIndex.ts
index df77c321c0..61d4a105ed 100644
--- a/superset-frontend/src/dashboard/util/getDirectPathToTabIndex.js
+++ b/superset-frontend/src/dashboard/util/getDirectPathToTabIndex.ts
@@ -16,7 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export default function getDirectPathToTabIndex(tabsComponent, tabIndex) {
+export interface TabsComponentLike {
+  id: string;
+  parents?: string[];
+  children: string[];
+  [key: string]: unknown;
+}
+
+export default function getDirectPathToTabIndex(
+  tabsComponent: TabsComponentLike,
+  tabIndex: number,
+): string[] {
   const directPathToFilter = (tabsComponent.parents || []).slice();
   directPathToFilter.push(tabsComponent.id);
   directPathToFilter.push(tabsComponent.children[tabIndex]);
diff --git a/superset-frontend/src/dashboard/util/getEmptyLayout.js b/superset-frontend/src/dashboard/util/getEmptyLayout.ts
similarity index 74%
rename from superset-frontend/src/dashboard/util/getEmptyLayout.js
rename to superset-frontend/src/dashboard/util/getEmptyLayout.ts
index 8a672bad0f..2aa5d030db 100644
--- a/superset-frontend/src/dashboard/util/getEmptyLayout.js
+++ b/superset-frontend/src/dashboard/util/getEmptyLayout.ts
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { DASHBOARD_ROOT_TYPE, DASHBOARD_GRID_TYPE } from './componentTypes';
+import type { ComponentType } from '../types';
 
 import {
   DASHBOARD_GRID_ID,
@@ -24,7 +25,22 @@ import {
   DASHBOARD_VERSION_KEY,
 } from './constants';
 
-export default function getEmptyLayout() {
+// Basic layout item for empty dashboard (simplified version without meta)
+interface BasicLayoutItem {
+  type: ComponentType;
+  id: string;
+  children: string[];
+  parents?: string[];
+}
+
+// Empty layout structure
+type EmptyLayout = {
+  [DASHBOARD_VERSION_KEY]: string;
+  [DASHBOARD_ROOT_ID]: BasicLayoutItem;
+  [DASHBOARD_GRID_ID]: BasicLayoutItem;
+};
+
+export default function getEmptyLayout(): EmptyLayout {
   return {
     [DASHBOARD_VERSION_KEY]: 'v2',
     [DASHBOARD_ROOT_ID]: {
diff --git a/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.test.ts b/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.test.ts
new file mode 100644
index 0000000000..6aef6c06d4
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.test.ts
@@ -0,0 +1,78 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import getKeyForFilterScopeTree from './getKeyForFilterScopeTree';
+
+describe('getKeyForFilterScopeTree', () => {
+  test('should return stringified activeFilterField array when activeFilterField is provided', () => {
+    const props = {
+      activeFilterField: 'filter1',
+      checkedFilterFields: ['filter2', 'filter3'],
+    };
+
+    const result = getKeyForFilterScopeTree(props);
+    expect(result).toBe('["filter1"]');
+  });
+
+  test('should return stringified checkedFilterFields when activeFilterField is not provided', () => {
+    const props = {
+      checkedFilterFields: ['filter2', 'filter3'],
+    };
+
+    const result = getKeyForFilterScopeTree(props);
+    expect(result).toBe('["filter2","filter3"]');
+  });
+
+  test('should return stringified checkedFilterFields when activeFilterField is undefined', () => {
+    const props = {
+      activeFilterField: undefined,
+      checkedFilterFields: ['filter1'],
+    };
+
+    const result = getKeyForFilterScopeTree(props);
+    expect(result).toBe('["filter1"]');
+  });
+
+  test('should return stringified empty array when both fields are empty', () => {
+    const props = {
+      checkedFilterFields: [],
+    };
+
+    const result = getKeyForFilterScopeTree(props);
+    expect(result).toBe('[]');
+  });
+
+  test('should handle single checked filter field', () => {
+    const props = {
+      checkedFilterFields: ['singleFilter'],
+    };
+
+    const result = getKeyForFilterScopeTree(props);
+    expect(result).toBe('["singleFilter"]');
+  });
+
+  test('should prioritize activeFilterField over checkedFilterFields', () => {
+    const props = {
+      activeFilterField: 'activeFilter',
+      checkedFilterFields: ['checked1', 'checked2'],
+    };
+
+    const result = getKeyForFilterScopeTree(props);
+    expect(result).toBe('["activeFilter"]');
+  });
+});
diff --git a/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.js b/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.ts
similarity index 87%
rename from superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.js
rename to superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.ts
index 808bbf8ef0..2a4962c269 100644
--- a/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.js
+++ b/superset-frontend/src/dashboard/util/getKeyForFilterScopeTree.ts
@@ -18,10 +18,15 @@
  */
 import { safeStringify } from '../../utils/safeStringify';
 
+interface GetKeyForFilterScopeTreeProps {
+  activeFilterField?: string;
+  checkedFilterFields: string[];
+}
+
 export default function getKeyForFilterScopeTree({
   activeFilterField,
   checkedFilterFields,
-}) {
+}: GetKeyForFilterScopeTreeProps): string {
   return safeStringify(
     activeFilterField ? [activeFilterField] : checkedFilterFields,
   );
diff --git a/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.test.ts b/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.test.ts
new file mode 100644
index 0000000000..38a2a4f86e
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.test.ts
@@ -0,0 +1,105 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import getLayoutComponentFromChartId from './getLayoutComponentFromChartId';
+import { CHART_TYPE, DASHBOARD_ROOT_TYPE } from './componentTypes';
+import type { DashboardLayout, LayoutItem } from '../types';
+
+const mockLayoutItem: LayoutItem = {
+  id: 'CHART-123',
+  type: CHART_TYPE,
+  children: [],
+  meta: {
+    chartId: 456,
+    defaultText: '',
+    height: 400,
+    placeholder: '',
+    sliceName: 'Test Chart',
+    text: '',
+    uuid: 'abc-def-ghi',
+    width: 400,
+  },
+};
+
+const mockRootLayoutItem: LayoutItem = {
+  id: 'ROOT_ID',
+  type: DASHBOARD_ROOT_TYPE,
+  children: ['CHART-123'],
+  meta: {
+    chartId: 0,
+    defaultText: '',
+    height: 0,
+    placeholder: '',
+    text: '',
+    uuid: 'root-uuid',
+    width: 0,
+  },
+};
+
+const mockLayout: DashboardLayout = {
+  'CHART-123': mockLayoutItem,
+  ROOT_ID: mockRootLayoutItem,
+};
+
+test('should find layout component by chart ID', () => {
+  const result = getLayoutComponentFromChartId(mockLayout, 456);
+  expect(result).toEqual(mockLayoutItem);
+});
+
+test('should return undefined when chart ID is not found', () => {
+  const result = getLayoutComponentFromChartId(mockLayout, 999);
+  expect(result).toBeUndefined();
+});
+
+test('should return undefined when layout is empty', () => {
+  const result = getLayoutComponentFromChartId({}, 456);
+  expect(result).toBeUndefined();
+});
+
+test('should ignore non-chart components', () => {
+  const layoutWithoutChart: DashboardLayout = {
+    ROOT_ID: mockRootLayoutItem,
+  };
+
+  const result = getLayoutComponentFromChartId(layoutWithoutChart, 456);
+  expect(result).toBeUndefined();
+});
+
+test('should handle components without meta', () => {
+  const componentWithoutMeta: LayoutItem = {
+    id: 'NO-META',
+    type: CHART_TYPE,
+    children: [],
+    meta: {
+      chartId: 0,
+      defaultText: '',
+      height: 0,
+      placeholder: '',
+      text: '',
+      uuid: 'no-meta-uuid',
+      width: 0,
+    },
+  };
+
+  const layoutWithoutMeta: DashboardLayout = {
+    'NO-META': componentWithoutMeta,
+  };
+
+  const result = getLayoutComponentFromChartId(layoutWithoutMeta, 456);
+  expect(result).toBeUndefined();
+});
diff --git a/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.js b/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.ts
similarity index 85%
rename from superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.js
rename to superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.ts
index 42d317adaf..6f1e6364cb 100644
--- a/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.js
+++ b/superset-frontend/src/dashboard/util/getLayoutComponentFromChartId.ts
@@ -18,8 +18,12 @@
  */
 /* eslint-disable no-param-reassign */
 import { CHART_TYPE } from './componentTypes';
+import type { DashboardLayout, LayoutItem } from '../types';
 
-export default function getLayoutComponentFromChartId(layout, chartId) {
+export default function getLayoutComponentFromChartId(
+  layout: DashboardLayout,
+  chartId: number,
+): LayoutItem | undefined {
   return Object.values(layout).find(
     currentComponent =>
       currentComponent &&
diff --git a/superset-frontend/src/dashboard/util/getLeafComponentIdFromPath.js b/superset-frontend/src/dashboard/util/getLeafComponentIdFromPath.ts
similarity index 84%
rename from superset-frontend/src/dashboard/util/getLeafComponentIdFromPath.js
rename to superset-frontend/src/dashboard/util/getLeafComponentIdFromPath.ts
index 37bed77c50..ea738498e9 100644
--- a/superset-frontend/src/dashboard/util/getLeafComponentIdFromPath.js
+++ b/superset-frontend/src/dashboard/util/getLeafComponentIdFromPath.ts
@@ -18,7 +18,9 @@
  */
 import { IN_COMPONENT_ELEMENT_TYPES } from './constants';
 
-export default function getLeafComponentIdFromPath(directPathToChild = []) {
+export default function getLeafComponentIdFromPath(
+  directPathToChild: string[] = [],
+): string | null {
   if (directPathToChild.length > 0) {
     const currentPath = directPathToChild.slice();
 
@@ -26,7 +28,10 @@ export default function getLeafComponentIdFromPath(directPathToChild = []) {
       const componentId = currentPath.pop();
       const componentType = componentId && componentId.split('-')[0];
 
-      if (!IN_COMPONENT_ELEMENT_TYPES.includes(componentType)) {
+      if (
+        componentType &&
+        !IN_COMPONENT_ELEMENT_TYPES.includes(componentType)
+      ) {
         return componentId;
       }
     }
diff --git a/superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts b/superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts
index 411f0a3f91..0ebf5b0ed8 100644
--- a/superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts
+++ b/superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts
@@ -20,7 +20,7 @@ import isDashboardEmpty from 'src/dashboard/util/isDashboardEmpty';
 import getEmptyLayout from 'src/dashboard/util/getEmptyLayout';
 
 describe('isDashboardEmpty', () => {
-  const emptyLayout: object = getEmptyLayout();
+  const emptyLayout = getEmptyLayout();
   const testLayout: object = {
     ...emptyLayout,
     'MARKDOWN-IhTGLhyiTd': {
diff --git a/superset-frontend/src/dashboard/util/isDashboardLoading.js b/superset-frontend/src/dashboard/util/isDashboardLoading.js
deleted file mode 100644
index c970fc8978..0000000000
--- a/superset-frontend/src/dashboard/util/isDashboardLoading.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-export default function isDashboardLoading(charts) {
-  return Object.values(charts).some(
-    chart => chart.chartUpdateStartTime > (chart.chartUpdateEndTime || 0),
-  );
-}
diff --git a/superset-frontend/src/dashboard/util/isDashboardLoading.test.ts b/superset-frontend/src/dashboard/util/isDashboardLoading.test.ts
new file mode 100644
index 0000000000..ee6aa85915
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/isDashboardLoading.test.ts
@@ -0,0 +1,48 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import isDashboardLoading, { ChartLoadTimestamps } from './isDashboardLoading';
+
+describe('isDashboardLoading', () => {
+  it('returns false when no charts are loading', () => {
+    const charts: Record<string, ChartLoadTimestamps> = {
+      a: { chartUpdateStartTime: 1, chartUpdateEndTime: 2 },
+      b: { chartUpdateStartTime: 5, chartUpdateEndTime: 5 },
+    };
+    expect(isDashboardLoading(charts)).toBe(false);
+  });
+
+  it('returns true when any chart has start > end', () => {
+    const charts: Record<string, ChartLoadTimestamps> = {
+      a: { chartUpdateStartTime: 10, chartUpdateEndTime: 5 },
+      b: { chartUpdateStartTime: 1, chartUpdateEndTime: 2 },
+    };
+    expect(isDashboardLoading(charts)).toBe(true);
+  });
+
+  it('treats missing end as 0', () => {
+    const charts: Record<string, ChartLoadTimestamps> = {
+      a: { chartUpdateStartTime: 1 },
+    };
+    expect(isDashboardLoading(charts)).toBe(true);
+  });
+
+  it('handles empty charts object', () => {
+    expect(isDashboardLoading({})).toBe(false);
+  });
+});
diff --git a/superset-frontend/src/dashboard/util/getChartIdsFromLayout.js b/superset-frontend/src/dashboard/util/isDashboardLoading.ts
similarity index 63%
rename from superset-frontend/src/dashboard/util/getChartIdsFromLayout.js
rename to superset-frontend/src/dashboard/util/isDashboardLoading.ts
index ac3931b355..ce5ffdc612 100644
--- a/superset-frontend/src/dashboard/util/getChartIdsFromLayout.js
+++ b/superset-frontend/src/dashboard/util/isDashboardLoading.ts
@@ -16,18 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { CHART_TYPE } from './componentTypes';
+export interface ChartLoadTimestamps {
+  chartUpdateStartTime?: number;
+  chartUpdateEndTime?: number | null;
+  // allow extra fields without narrowing
+  [key: string]: unknown;
+}
 
-export default function getChartIdsFromLayout(layout) {
-  return Object.values(layout).reduce((chartIds, currentComponent) => {
-    if (
-      currentComponent &&
-      currentComponent.type === CHART_TYPE &&
-      currentComponent.meta &&
-      currentComponent.meta.chartId
-    ) {
-      chartIds.push(currentComponent.meta.chartId);
-    }
-    return chartIds;
-  }, []);
+export default function isDashboardLoading(
+  charts: Record<string, ChartLoadTimestamps>,
+): boolean {
+  return Object.values(charts).some(chart => {
+    const start = chart.chartUpdateStartTime ?? 0;
+    const end = chart.chartUpdateEndTime ?? 0;
+    return start > end;
+  });
 }
diff --git a/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.test.ts b/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.test.ts
new file mode 100644
index 0000000000..e01299a6c4
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.test.ts
@@ -0,0 +1,197 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import isInDifferentFilterScopes from './isInDifferentFilterScopes';
+
+test('returns false when no dashboard filters are provided', () => {
+  const result = isInDifferentFilterScopes({
+    dashboardFilters: {},
+    source: ['tab1', 'tab2'],
+    destination: ['tab2', 'tab3'],
+  });
+
+  expect(result).toBe(false);
+});
+
+test('returns false when source and destination are in same filter scopes', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1', 'tab2'],
+        },
+        column2: {
+          scope: ['tab3', 'tab4'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: ['tab1'],
+    destination: ['tab1'],
+  });
+
+  expect(result).toBe(false);
+});
+
+test('returns true when source and destination are in different filter scopes', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1', 'tab2'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: ['tab1'],
+    destination: ['tab3'],
+  });
+
+  expect(result).toBe(true);
+});
+
+test('returns true when one is in scope and the other is not', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: ['tab1'], // in scope
+    destination: ['tab2'], // not in scope
+  });
+
+  expect(result).toBe(true);
+});
+
+test('handles multiple filters with complex scopes', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1', 'tab2'],
+        },
+        column2: {
+          scope: ['tab3'],
+        },
+      },
+    },
+    filter2: {
+      chartId: 456,
+      scopes: {
+        column1: {
+          scope: ['tab2', 'tab4'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: ['tab1'],
+    destination: ['tab4'],
+  });
+
+  expect(result).toBe(true);
+});
+
+test('handles empty source and destination arrays', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: [],
+    destination: [],
+  });
+
+  expect(result).toBe(false);
+});
+
+test('uses default parameters when not provided', () => {
+  const result = isInDifferentFilterScopes({});
+
+  expect(result).toBe(false);
+});
+
+test('returns true when source and destination have different presence in filter scopes', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1', 'tab2', 'tab3'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: ['tab1', 'tab2'],
+    destination: ['tab2', 'tab3'],
+  });
+
+  // tab1 is in source but not destination, tab3 is in destination but not source
+  expect(result).toBe(true);
+});
+
+test('returns false when both source and destination contain same tabs', () => {
+  const dashboardFilters = {
+    filter1: {
+      chartId: 123,
+      scopes: {
+        column1: {
+          scope: ['tab1', 'tab2'],
+        },
+      },
+    },
+  };
+
+  const result = isInDifferentFilterScopes({
+    dashboardFilters,
+    source: ['tab1'],
+    destination: ['tab1'],
+  });
+
+  expect(result).toBe(false);
+});
diff --git a/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.js b/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.ts
similarity index 65%
rename from superset-frontend/src/dashboard/util/isInDifferentFilterScopes.js
rename to superset-frontend/src/dashboard/util/isInDifferentFilterScopes.ts
index 3789d07ff2..99b22ded9e 100644
--- a/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.js
+++ b/superset-frontend/src/dashboard/util/isInDifferentFilterScopes.ts
@@ -16,17 +16,40 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+
+// Dashboard filter structure based on the actual usage pattern
+interface DashboardFilterColumn {
+  scope: string[];
+}
+
+interface DashboardFilter {
+  chartId: number;
+  scopes: Record<string, DashboardFilterColumn>;
+}
+
+interface DashboardFilters {
+  [filterId: string]: DashboardFilter;
+}
+
+interface IsInDifferentFilterScopesProps {
+  dashboardFilters?: DashboardFilters;
+  source?: string[];
+  destination?: string[];
+}
+
 export default function isInDifferentFilterScopes({
   dashboardFilters = {},
   source = [],
   destination = [],
-}) {
+}: IsInDifferentFilterScopesProps): boolean {
   const sourceSet = new Set(source);
   const destinationSet = new Set(destination);
 
-  const allScopes = [].concat(
+  const allScopes = ([] as string[]).concat(
     ...Object.values(dashboardFilters).map(({ scopes }) =>
-      [].concat(...Object.values(scopes).map(({ scope }) => scope)),
+      ([] as string[]).concat(
+        ...Object.values(scopes).map(({ scope }) => scope),
+      ),
     ),
   );
   return allScopes.some(tab => destinationSet.has(tab) !== sourceSet.has(tab));
diff --git a/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.test.ts b/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.test.ts
new file mode 100644
index 0000000000..3e492eb4ef
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.test.ts
@@ -0,0 +1,246 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ChartState } from 'src/explore/types';
+import { Layout } from 'src/dashboard/types';
+import childChartsDidLoad from './childChartsDidLoad';
+
+import mockFindNonTabChildChartIdsImport from './findNonTabChildChartIds';
+
+// Mock the findNonTabChildChartIds dependency
+jest.mock('./findNonTabChildChartIds', () => ({
+  __esModule: true,
+  default: jest.fn(),
+}));
+
+const mockFindNonTabChildChartIds =
+  mockFindNonTabChildChartIdsImport as jest.MockedFunction<
+    typeof mockFindNonTabChildChartIdsImport
+  >;
+
+describe('childChartsDidLoad', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  test('returns didLoad true when all charts are in completed states', () => {
+    const chartIds = [1, 2, 3];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'rendered', chartUpdateStartTime: 100 },
+      '2': { chartStatus: 'stopped', chartUpdateStartTime: 200 },
+      '3': { chartStatus: 'failed', chartUpdateStartTime: 150 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(true);
+    expect(result.minQueryStartTime).toBe(100);
+    expect(mockFindNonTabChildChartIds).toHaveBeenCalledWith({
+      id: 'test-id',
+      layout,
+    });
+  });
+
+  test('returns didLoad false when some charts are in loading state', () => {
+    const chartIds = [1, 2, 3];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'rendered', chartUpdateStartTime: 100 },
+      '2': { chartStatus: 'loading', chartUpdateStartTime: 200 },
+      '3': { chartStatus: 'failed', chartUpdateStartTime: 150 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(false);
+    expect(result.minQueryStartTime).toBe(100);
+  });
+
+  test('handles missing chart queries gracefully', () => {
+    const chartIds = [1, 2, 3];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'rendered', chartUpdateStartTime: 100 },
+      // Chart 2 is missing from queries
+      '3': { chartStatus: 'failed', chartUpdateStartTime: 150 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(false);
+    expect(result.minQueryStartTime).toBe(100);
+  });
+
+  test('handles empty chart queries object', () => {
+    const chartIds = [1, 2, 3];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {};
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(false);
+    expect(result.minQueryStartTime).toBe(Infinity);
+  });
+
+  test('handles empty chart IDs array', () => {
+    const chartIds: number[] = [];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'rendered', chartUpdateStartTime: 100 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(true); // every() returns true for empty array
+    expect(result.minQueryStartTime).toBe(Infinity);
+  });
+
+  test('calculates minimum query start time correctly', () => {
+    const chartIds = [1, 2, 3, 4];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'rendered', chartUpdateStartTime: 500 },
+      '2': { chartStatus: 'stopped', chartUpdateStartTime: 100 },
+      '3': { chartStatus: 'failed', chartUpdateStartTime: 300 },
+      '4': { chartStatus: 'rendered', chartUpdateStartTime: 200 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(true);
+    expect(result.minQueryStartTime).toBe(100);
+  });
+
+  test('handles charts with missing chartUpdateStartTime', () => {
+    const chartIds = [1, 2];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'rendered' }, // Missing chartUpdateStartTime
+      '2': { chartStatus: 'stopped', chartUpdateStartTime: 200 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(true);
+    expect(result.minQueryStartTime).toBe(200);
+  });
+
+  test('handles charts with null chartStatus', () => {
+    const chartIds = [1, 2];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: null, chartUpdateStartTime: 100 },
+      '2': { chartStatus: 'stopped', chartUpdateStartTime: 200 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(false); // null chartStatus is not in the completed states
+    expect(result.minQueryStartTime).toBe(100);
+  });
+
+  test('recognizes all valid completed chart states', () => {
+    const chartIds = [1, 2, 3];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'stopped', chartUpdateStartTime: 100 },
+      '2': { chartStatus: 'failed', chartUpdateStartTime: 200 },
+      '3': { chartStatus: 'rendered', chartUpdateStartTime: 150 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(true);
+    expect(result.minQueryStartTime).toBe(100);
+  });
+
+  test('does not recognize incomplete chart states', () => {
+    const chartIds = [1, 2];
+    const layout: Layout = {};
+    const chartQueries: Record<string, Partial<ChartState>> = {
+      '1': { chartStatus: 'loading', chartUpdateStartTime: 100 },
+      '2': { chartStatus: 'success', chartUpdateStartTime: 200 },
+    };
+
+    mockFindNonTabChildChartIds.mockReturnValue(chartIds);
+
+    const result = childChartsDidLoad({
+      chartQueries,
+      layout,
+      id: 'test-id',
+    });
+
+    expect(result.didLoad).toBe(false); // 'loading' and 'success' are not in completed states
+    expect(result.minQueryStartTime).toBe(100);
+  });
+});
diff --git a/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js b/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.ts
similarity index 57%
rename from superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js
rename to superset-frontend/src/dashboard/util/logging/childChartsDidLoad.ts
index a23958da0c..3145e87cb2 100644
--- a/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.js
+++ b/superset-frontend/src/dashboard/util/logging/childChartsDidLoad.ts
@@ -16,16 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { ChartState } from 'src/explore/types';
+import { Layout } from 'src/dashboard/types';
 import findNonTabChildCharIds from './findNonTabChildChartIds';
 
-export default function childChartsDidLoad({ chartQueries, layout, id }) {
+interface ChildChartsDidLoadParams {
+  chartQueries: Record<string, Partial<ChartState>>;
+  layout: Layout;
+  id: string;
+}
+
+interface ChildChartsDidLoadResult {
+  didLoad: boolean;
+  minQueryStartTime: number;
+}
+
+export default function childChartsDidLoad({
+  chartQueries,
+  layout,
+  id,
+}: ChildChartsDidLoadParams): ChildChartsDidLoadResult {
   const chartIds = findNonTabChildCharIds({ id, layout });
 
   let minQueryStartTime = Infinity;
-  const didLoad = chartIds.every(chartId => {
-    const query = chartQueries[chartId] || {};
-    minQueryStartTime = Math.min(query.chartUpdateStartTime, minQueryStartTime);
-    return ['stopped', 'failed', 'rendered'].indexOf(query.chartStatus) > -1;
+  const didLoad = chartIds.every((chartId: number) => {
+    const query = chartQueries[chartId.toString()] || {};
+    minQueryStartTime = Math.min(
+      query.chartUpdateStartTime ?? Infinity,
+      minQueryStartTime,
+    );
+    return ['stopped', 'failed', 'rendered'].includes(query.chartStatus || '');
   });
 
   return { didLoad, minQueryStartTime };
diff --git a/superset-frontend/src/dashboard/util/serializeFilterScopes.test.ts b/superset-frontend/src/dashboard/util/serializeFilterScopes.test.ts
new file mode 100644
index 0000000000..e10893044d
--- /dev/null
+++ b/superset-frontend/src/dashboard/util/serializeFilterScopes.test.ts
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import serializeFilterScopes from './serializeFilterScopes';
+
+const mockDashboardFilters = {
+  '1': {
+    chartId: 'chart_1',
+    scopes: {
+      column1: {
+        scope: ['ROOT_ID'],
+        immune: [],
+      },
+      column2: {
+        scope: ['ROOT_ID', 'TAB_1'],
+        immune: ['chart_2'],
+      },
+    },
+  },
+  '2': {
+    chartId: 'chart_2',
+    scopes: {
+      region: {
+        scope: ['ROOT_ID'],
+        immune: [],
+      },
+    },
+  },
+};
+
+describe('serializeFilterScopes', () => {
+  test('should serialize dashboard filter scopes correctly', () => {
+    const result = serializeFilterScopes(mockDashboardFilters);
+
+    expect(result).toEqual({
+      chart_1: {
+        column1: {
+          scope: ['ROOT_ID'],
+          immune: [],
+        },
+        column2: {
+          scope: ['ROOT_ID', 'TAB_1'],
+          immune: ['chart_2'],
+        },
+      },
+      chart_2: {
+        region: {
+          scope: ['ROOT_ID'],
+          immune: [],
+        },
+      },
+    });
+  });
+
+  test('should handle empty dashboardFilters', () => {
+    const result = serializeFilterScopes({});
+    expect(result).toEqual({});
+  });
+
+  test('should handle filters with no scopes', () => {
+    const filtersWithEmptyScopes = {
+      '1': {
+        chartId: 'chart_1',
+        scopes: {},
+      },
+    };
+
+    const result = serializeFilterScopes(filtersWithEmptyScopes);
+    expect(result).toEqual({
+      chart_1: {},
+    });
+  });
+
+  test('should handle numeric chart IDs', () => {
+    const filtersWithNumericIds = {
+      '1': {
+        chartId: 123,
+        scopes: {
+          column1: {
+            scope: ['ROOT_ID'],
+            immune: [],
+          },
+        },
+      },
+    };
+
+    const result = serializeFilterScopes(filtersWithNumericIds);
+    expect(result).toEqual({
+      123: {
+        column1: {
+          scope: ['ROOT_ID'],
+          immune: [],
+        },
+      },
+    });
+  });
+});
diff --git a/superset-frontend/src/dashboard/util/serializeFilterScopes.js b/superset-frontend/src/dashboard/util/serializeFilterScopes.ts
similarity index 68%
rename from superset-frontend/src/dashboard/util/serializeFilterScopes.js
rename to superset-frontend/src/dashboard/util/serializeFilterScopes.ts
index 303171f24f..75fe907978 100644
--- a/superset-frontend/src/dashboard/util/serializeFilterScopes.js
+++ b/superset-frontend/src/dashboard/util/serializeFilterScopes.ts
@@ -16,7 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-export default function serializeFilterScopes(dashboardFilters) {
+import { JsonObject } from '@superset-ui/core';
+
+interface DashboardFilterScope {
+  scope: string[] | JsonObject;
+  immune?: string[];
+}
+
+interface DashboardFilter {
+  chartId: number | string;
+  scopes: Record<string, DashboardFilterScope>;
+}
+
+interface DashboardFilters {
+  [filterId: string]: DashboardFilter;
+}
+
+interface SerializedFilterScopes {
+  [chartId: string]: Record<string, DashboardFilterScope>;
+}
+
+export default function serializeFilterScopes(
+  dashboardFilters: DashboardFilters,
+): SerializedFilterScopes {
   return Object.values(dashboardFilters).reduce((map, { chartId, scopes }) => {
     const scopesById = Object.keys(scopes).reduce(
       (scopesByColumn, column) => ({
diff --git a/superset-frontend/src/dashboard/util/shouldWrapChildInRow.js b/superset-frontend/src/dashboard/util/shouldWrapChildInRow.ts
similarity index 65%
rename from superset-frontend/src/dashboard/util/shouldWrapChildInRow.js
rename to superset-frontend/src/dashboard/util/shouldWrapChildInRow.ts
index 5f5f695228..17ebcc844d 100644
--- a/superset-frontend/src/dashboard/util/shouldWrapChildInRow.js
+++ b/superset-frontend/src/dashboard/util/shouldWrapChildInRow.ts
@@ -23,8 +23,20 @@ import {
   MARKDOWN_TYPE,
   TAB_TYPE,
 } from './componentTypes';
+import { ComponentType } from '../types';
 
-const typeToWrapChildLookup = {
+interface WrapChildParams {
+  parentType: ComponentType | undefined | null;
+  childType: ComponentType | undefined | null;
+}
+
+type ParentTypes = typeof DASHBOARD_GRID_TYPE | typeof TAB_TYPE;
+type ChildTypes = typeof CHART_TYPE | typeof COLUMN_TYPE | typeof MARKDOWN_TYPE;
+
+const typeToWrapChildLookup: Record<
+  ParentTypes,
+  Record<ChildTypes, boolean>
+> = {
   [DASHBOARD_GRID_TYPE]: {
     [CHART_TYPE]: true,
     [COLUMN_TYPE]: true,
@@ -38,11 +50,14 @@ const typeToWrapChildLookup = {
   },
 };
 
-export default function shouldWrapChildInRow({ parentType, childType }) {
+export default function shouldWrapChildInRow({
+  parentType,
+  childType,
+}: WrapChildParams): boolean {
   if (!parentType || !childType) return false;
 
-  const wrapChildLookup = typeToWrapChildLookup[parentType];
+  const wrapChildLookup = typeToWrapChildLookup[parentType as ParentTypes];
   if (!wrapChildLookup) return false;
 
-  return Boolean(wrapChildLookup[childType]);
+  return Boolean(wrapChildLookup[childType as ChildTypes]);
 }
diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
index 4552a43a79..7f71633c3c 100644
--- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
+++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx
@@ -58,6 +58,7 @@ export interface IDatasource {
   sql?: string | null;
   datasource_name?: string | null;
   name?: string | null;
+  catalog?: string | null;
   schema?: string | null;
 }
 
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.js b/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.js
deleted file mode 100644
index b8900c1756..0000000000
--- a/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import PropTypes from 'prop-types';
-import { Clauses, ExpressionTypes } from './types';
-
-export default PropTypes.oneOfType([
-  PropTypes.shape({
-    expressionType: PropTypes.oneOf([ExpressionTypes.Simple]).isRequired,
-    clause: PropTypes.oneOf([Clauses.Having, Clauses.Where]).isRequired,
-    subject: PropTypes.string.isRequired,
-    comparator: PropTypes.oneOfType([
-      PropTypes.string,
-      PropTypes.arrayOf(PropTypes.string),
-    ]).isRequired,
-  }),
-  PropTypes.shape({
-    expressionType: PropTypes.oneOf([ExpressionTypes.Sql]).isRequired,
-    clause: PropTypes.oneOf([Clauses.Where, Clauses.Having]).isRequired,
-    sqlExpression: PropTypes.string.isRequired,
-  }),
-]);
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.test.ts b/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.test.ts
new file mode 100644
index 0000000000..5e96174c41
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.test.ts
@@ -0,0 +1,103 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  AdhocFilterSimple,
+  AdhocFilterSql,
+  AdhocFilterType,
+} from './adhocFilterType';
+import { Clauses, ExpressionTypes } from './types';
+
+describe('adhocFilterType', () => {
+  test('should accept simple adhoc filter type', () => {
+    const simpleFilter: AdhocFilterSimple = {
+      expressionType: ExpressionTypes.Simple,
+      clause: Clauses.Where,
+      subject: 'column_name',
+      comparator: 'test_value',
+    };
+
+    expect(simpleFilter.expressionType).toBe(ExpressionTypes.Simple);
+    expect(simpleFilter.clause).toBe(Clauses.Where);
+    expect(simpleFilter.subject).toBe('column_name');
+    expect(simpleFilter.comparator).toBe('test_value');
+  });
+
+  test('should accept SQL adhoc filter type', () => {
+    const sqlFilter: AdhocFilterSql = {
+      expressionType: ExpressionTypes.Sql,
+      clause: Clauses.Having,
+      sqlExpression: 'COUNT(*) > 5',
+    };
+
+    expect(sqlFilter.expressionType).toBe(ExpressionTypes.Sql);
+    expect(sqlFilter.clause).toBe(Clauses.Having);
+    expect(sqlFilter.sqlExpression).toBe('COUNT(*) > 5');
+  });
+
+  test('should accept both simple and SQL filters as AdhocFilterType', () => {
+    const simpleFilter: AdhocFilterType = {
+      expressionType: ExpressionTypes.Simple,
+      clause: Clauses.Where,
+      subject: 'column_name',
+      comparator: ['value1', 'value2'],
+    };
+
+    const sqlFilter: AdhocFilterType = {
+      expressionType: ExpressionTypes.Sql,
+      clause: Clauses.Having,
+      sqlExpression: 'AVG(sales) > 1000',
+    };
+
+    expect(simpleFilter.expressionType).toBe(ExpressionTypes.Simple);
+    expect(sqlFilter.expressionType).toBe(ExpressionTypes.Sql);
+  });
+
+  test('should handle array comparator for simple filters', () => {
+    const filterWithArrayComparator: AdhocFilterSimple = {
+      expressionType: ExpressionTypes.Simple,
+      clause: Clauses.Where,
+      subject: 'category',
+      comparator: ['A', 'B', 'C'],
+    };
+
+    expect(Array.isArray(filterWithArrayComparator.comparator)).toBe(true);
+    expect(filterWithArrayComparator.comparator).toEqual(['A', 'B', 'C']);
+  });
+
+  test('should handle optional properties', () => {
+    const filterWithOptionalProps: AdhocFilterSimple = {
+      expressionType: ExpressionTypes.Simple,
+      clause: Clauses.Where,
+      subject: 'column_name',
+      comparator: 'test_value',
+      operator: 'EQUALS',
+      operatorId: 'EQUALS',
+      isExtra: true,
+      isNew: false,
+      datasourceWarning: false,
+      deck_slices: [1, 2, 3],
+      layerFilterScope: 'global',
+      filterOptionName: 'custom_filter_name',
+    };
+
+    expect(filterWithOptionalProps.operator).toBe('EQUALS');
+    expect(filterWithOptionalProps.isExtra).toBe(true);
+    expect(filterWithOptionalProps.deck_slices).toEqual([1, 2, 3]);
+  });
+});
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.ts b/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.ts
new file mode 100644
index 0000000000..df2e663f7a
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/FilterControl/adhocFilterType.ts
@@ -0,0 +1,64 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { InferProps } from 'prop-types';
+import { Clauses, ExpressionTypes } from './types';
+
+export interface AdhocFilterSimple {
+  expressionType: ExpressionTypes.Simple;
+  clause: Clauses.Having | Clauses.Where;
+  subject: string;
+  comparator: string | string[];
+  operator?: string;
+  operatorId?: string;
+  isExtra?: boolean;
+  isNew?: boolean;
+  datasourceWarning?: boolean;
+  deck_slices?: number[];
+  layerFilterScope?: string;
+  filterOptionName?: string;
+}
+
+export interface AdhocFilterSql {
+  expressionType: ExpressionTypes.Sql;
+  clause: Clauses.Where | Clauses.Having;
+  sqlExpression: string;
+  subject?: string | null;
+  operator?: string | null;
+  operatorId?: string;
+  comparator?: null;
+  isExtra?: boolean;
+  isNew?: boolean;
+  datasourceWarning?: boolean;
+  deck_slices?: number[];
+  layerFilterScope?: string;
+  filterOptionName?: string;
+}
+
+export type AdhocFilterType = AdhocFilterSimple | AdhocFilterSql;
+
+// PropTypes validation function - babel-plugin-typescript-to-proptypes automatically
+// generates PropTypes from the TypeScript interface above
+export default function AdhocFilterValidator(props: {
+  filter: AdhocFilterType;
+}) {
+  return null; // PropTypes auto-generated by babel plugin
+}
+
+// For consumers needing PropTypes type inference
+export type AdhocFilterProps = InferProps<typeof AdhocFilterValidator>;
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/columnType.test.ts b/superset-frontend/src/explore/components/controls/FilterControl/columnType.test.ts
new file mode 100644
index 0000000000..da1ef5eb09
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/FilterControl/columnType.test.ts
@@ -0,0 +1,53 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ColumnType } from './columnType';
+
+test('ColumnType should have proper structure', () => {
+  const mockColumn: ColumnType = {
+    column_name: 'test_column',
+    type: 'STRING',
+  };
+
+  expect(mockColumn.column_name).toBe('test_column');
+  expect(mockColumn.type).toBe('STRING');
+});
+
+test('ColumnType should allow optional type field', () => {
+  const mockColumn: ColumnType = {
+    column_name: 'test_column',
+  };
+
+  expect(mockColumn.column_name).toBe('test_column');
+  expect(mockColumn.type).toBeUndefined();
+});
+
+test('ColumnType should work with different type values', () => {
+  const stringColumn: ColumnType = {
+    column_name: 'str_col',
+    type: 'STRING',
+  };
+
+  const numericColumn: ColumnType = {
+    column_name: 'num_col',
+    type: 'NUMERIC',
+  };
+
+  expect(stringColumn.type).toBe('STRING');
+  expect(numericColumn.type).toBe('NUMERIC');
+});
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/columnType.js b/superset-frontend/src/explore/components/controls/FilterControl/columnType.ts
similarity index 76%
rename from superset-frontend/src/explore/components/controls/MetricControl/columnType.js
rename to superset-frontend/src/explore/components/controls/FilterControl/columnType.ts
index bb136fb58f..e453115b0b 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/columnType.js
+++ b/superset-frontend/src/explore/components/controls/FilterControl/columnType.ts
@@ -16,9 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import PropTypes from 'prop-types';
+import { Column } from '@superset-ui/core';
 
-export default PropTypes.shape({
-  column_name: PropTypes.string.isRequired,
-  type: PropTypes.string,
-});
+export type ColumnType = Pick<Column, 'column_name' | 'type'>;
+
+// For backward compatibility with PropTypes usage - create a placeholder object
+const columnType = {} as any;
+export default columnType;
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/types.ts b/superset-frontend/src/explore/components/controls/FilterControl/types.ts
index d2eebc5bff..869effd23a 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/FilterControl/types.ts
@@ -26,3 +26,10 @@ export enum Clauses {
   Having = 'HAVING',
   Where = 'WHERE',
 }
+
+// Re-export AdhocFilter types for convenient access
+export type {
+  AdhocFilterSimple,
+  AdhocFilterSql,
+  AdhocFilterType,
+} from './adhocFilterType';
diff --git a/superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts b/superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.test.ts
similarity index 51%
copy from superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts
copy to superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.test.ts
index 411f0a3f91..3e6001c213 100644
--- a/superset-frontend/src/dashboard/util/isDashboardEmpty.test.ts
+++ b/superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.test.ts
@@ -16,27 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import isDashboardEmpty from 'src/dashboard/util/isDashboardEmpty';
-import getEmptyLayout from 'src/dashboard/util/getEmptyLayout';
 
-describe('isDashboardEmpty', () => {
-  const emptyLayout: object = getEmptyLayout();
-  const testLayout: object = {
-    ...emptyLayout,
-    'MARKDOWN-IhTGLhyiTd': {
-      children: [],
-      id: 'MARKDOWN-IhTGLhyiTd',
-      meta: { code: 'test me', height: 50, width: 4 },
-      parents: ['ROOT_ID', 'GRID_ID', 'ROW-uPjcKNYJQy'],
-      type: 'MARKDOWN',
-    },
+import { AggregateOption } from './aggregateOptionType';
+
+test('AggregateOption type should enforce aggregate_name as string', () => {
+  // Test that the type can be properly used
+  const validAggregate: AggregateOption = {
+    aggregate_name: 'SUM',
   };
 
-  it('should return true for empty dashboard', () => {
-    expect(isDashboardEmpty(emptyLayout)).toBe(true);
-  });
+  expect(typeof validAggregate.aggregate_name).toBe('string');
+  expect(validAggregate.aggregate_name).toBe('SUM');
+});
+
+test('AggregateOption should work with various aggregate names', () => {
+  const aggregates: AggregateOption[] = [
+    { aggregate_name: 'COUNT' },
+    { aggregate_name: 'AVG' },
+    { aggregate_name: 'MIN' },
+    { aggregate_name: 'MAX' },
+  ];
 
-  it('should return false for non-empty dashboard', () => {
-    expect(isDashboardEmpty(testLayout)).toBe(false);
+  aggregates.forEach(aggregate => {
+    expect(typeof aggregate.aggregate_name).toBe('string');
+    expect(aggregate.aggregate_name.length).toBeGreaterThan(0);
   });
 });
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/columnType.js b/superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.ts
similarity index 84%
rename from superset-frontend/src/explore/components/controls/FilterControl/columnType.js
rename to superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.ts
index bb136fb58f..c157910ad9 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/columnType.js
+++ b/superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.ts
@@ -16,9 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import PropTypes from 'prop-types';
 
-export default PropTypes.shape({
-  column_name: PropTypes.string.isRequired,
-  type: PropTypes.string,
-});
+export type { AggregateOption } from './types';
+
+// For backward compatibility with PropTypes usage
+export { AggregateOption as default } from './types';
diff --git a/superset-frontend/src/explore/components/controls/FilterControl/types.ts b/superset-frontend/src/explore/components/controls/MetricControl/columnType.ts
similarity index 76%
copy from superset-frontend/src/explore/components/controls/FilterControl/types.ts
copy to superset-frontend/src/explore/components/controls/MetricControl/columnType.ts
index d2eebc5bff..e453115b0b 100644
--- a/superset-frontend/src/explore/components/controls/FilterControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/MetricControl/columnType.ts
@@ -16,13 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { Column } from '@superset-ui/core';
 
-export enum ExpressionTypes {
-  Simple = 'SIMPLE',
-  Sql = 'SQL',
-}
+export type ColumnType = Pick<Column, 'column_name' | 'type'>;
 
-export enum Clauses {
-  Having = 'HAVING',
-  Where = 'WHERE',
-}
+// For backward compatibility with PropTypes usage - create a placeholder object
+const columnType = {} as any;
+export default columnType;
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.js b/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.js
deleted file mode 100644
index 3c9a987bd1..0000000000
--- a/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import PropTypes from 'prop-types';
-
-export default PropTypes.shape({
-  metric_name: PropTypes.string,
-  verbose_name: PropTypes.string,
-  expression: PropTypes.string,
-});
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.test.ts b/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.test.ts
new file mode 100644
index 0000000000..91fc77f803
--- /dev/null
+++ b/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.test.ts
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { savedMetricType } from './savedMetricType';
+
+test('savedMetricType exports the correct type structure', () => {
+  // Type assertion test - if this compiles without errors,
+  // the type structure is correct
+  const validMetric: savedMetricType = {
+    metric_name: 'test_metric',
+    verbose_name: 'Test Metric',
+    expression: 'SUM(column)',
+  };
+
+  expect(validMetric.metric_name).toBe('test_metric');
+  expect(validMetric.verbose_name).toBe('Test Metric');
+  expect(validMetric.expression).toBe('SUM(column)');
+});
+
+test('savedMetricType allows optional verbose_name', () => {
+  // Test that verbose_name is optional
+  const validMetricMinimal: savedMetricType = {
+    metric_name: 'minimal_metric',
+    expression: 'COUNT(*)',
+  };
+
+  expect(validMetricMinimal.metric_name).toBe('minimal_metric');
+  expect(validMetricMinimal.expression).toBe('COUNT(*)');
+  expect(validMetricMinimal.verbose_name).toBeUndefined();
+});
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.js b/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.ts
similarity index 84%
rename from superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.js
rename to superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.ts
index 0075d2b74a..4ea5b3409a 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/aggregateOptionType.js
+++ b/superset-frontend/src/explore/components/controls/MetricControl/savedMetricType.ts
@@ -16,8 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import PropTypes from 'prop-types';
+export { savedMetricType } from './types';
 
-export default PropTypes.shape({
-  aggregate_name: PropTypes.string.isRequired,
-});
+// For backward compatibility with PropTypes usage
+export { savedMetricType as default } from './types';
diff --git a/superset-frontend/src/explore/components/controls/MetricControl/types.ts b/superset-frontend/src/explore/components/controls/MetricControl/types.ts
index f8f52fcb33..7e16810641 100644
--- a/superset-frontend/src/explore/components/controls/MetricControl/types.ts
+++ b/superset-frontend/src/explore/components/controls/MetricControl/types.ts
@@ -21,3 +21,7 @@ export type savedMetricType = {
   verbose_name?: string;
   expression: string;
 };
+
+export interface AggregateOption {
+  aggregate_name: string;
+}
diff --git a/superset-frontend/src/explore/controlPanels/Separator.test.ts b/superset-frontend/src/explore/controlPanels/Separator.test.ts
new file mode 100644
index 0000000000..42212c4927
--- /dev/null
+++ b/superset-frontend/src/explore/controlPanels/Separator.test.ts
@@ -0,0 +1,76 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import type {
+  ControlPanelState,
+  ControlState,
+} from '@superset-ui/chart-controls';
+import Separator from './Separator';
+
+function getCodeControlMapStateToProps() {
+  const sections =
+    (Separator.controlPanelSections as unknown as Array<{
+      controlSetRows?: Array<
+        Array<{
+          name?: string;
+          config?: {
+            mapStateToProps?: (s: Partial<ControlPanelState>) => {
+              language: string;
+            };
+          };
+        }>
+      >;
+    }>) || [];
+
+  const codeControl = sections
+    .flatMap(s => s.controlSetRows || [])
+    .flatMap(r => r)
+    .find(i => i?.name === 'code') as unknown as {
+    config: {
+      mapStateToProps: (s: Partial<ControlPanelState>) => { language: string };
+    };
+  };
+
+  if (!codeControl || !codeControl.config?.mapStateToProps) {
+    throw new Error('Code control configuration not found');
+  }
+  return codeControl.config.mapStateToProps;
+}
+
+describe('Separator control panel config', () => {
+  it('defaults language to markdown when markup_type is missing', () => {
+    const mapStateToProps = getCodeControlMapStateToProps();
+    const state: Partial<ControlPanelState> = {};
+    const result = mapStateToProps(state);
+    expect(result.language).toBe('markdown');
+  });
+
+  it('uses markup_type value when provided', () => {
+    const mapStateToProps = getCodeControlMapStateToProps();
+    const state: Partial<ControlPanelState> = {
+      controls: {
+        // minimal mock for the control used in mapStateToProps
+        markup_type: { value: 'html' } as Partial<
+          ControlState<'SelectControl'>
+        > as ControlState<'SelectControl'>,
+      },
+    };
+    const result = mapStateToProps(state);
+    expect(result.language).toBe('html');
+  });
+});
diff --git a/superset-frontend/src/explore/controlPanels/Separator.js b/superset-frontend/src/explore/controlPanels/Separator.ts
similarity index 79%
rename from superset-frontend/src/explore/controlPanels/Separator.js
rename to superset-frontend/src/explore/controlPanels/Separator.ts
index d49d389b90..170d09c997 100644
--- a/superset-frontend/src/explore/controlPanels/Separator.js
+++ b/superset-frontend/src/explore/controlPanels/Separator.ts
@@ -17,9 +17,13 @@
  * under the License.
  */
 import { t, validateNonEmpty } from '@superset-ui/core';
+import type {
+  ControlPanelConfig,
+  ControlPanelState,
+} from '@superset-ui/chart-controls';
 import { formatSelectOptions } from 'src/explore/exploreUtils';
 
-export default {
+const config: ControlPanelConfig = {
   controlPanelSections: [
     {
       label: t('Code'),
@@ -45,12 +49,15 @@ export default {
               type: 'TextAreaControl',
               label: t('Code'),
               description: t('Put your code here'),
-              mapStateToProps: state => ({
-                language:
-                  state.controls && state.controls.markup_type
-                    ? state.controls.markup_type.value
-                    : 'markdown',
-              }),
+              mapStateToProps: (state: Partial<ControlPanelState>) => {
+                const languageValue = state.controls?.markup_type?.value;
+                return {
+                  language:
+                    typeof languageValue === 'string'
+                      ? languageValue
+                      : 'markdown',
+                };
+              },
               default: '',
             },
           },
@@ -74,3 +81,5 @@ export default {
     },
   },
 };
+
+export default config;
diff --git a/superset-frontend/src/explore/controlPanels/timeGrainSqlaAnimationOverrides.js b/superset-frontend/src/explore/controlPanels/timeGrainSqlaAnimationOverrides.ts
similarity index 64%
rename from superset-frontend/src/explore/controlPanels/timeGrainSqlaAnimationOverrides.js
rename to superset-frontend/src/explore/controlPanels/timeGrainSqlaAnimationOverrides.ts
index b7338e10cf..1aef2dbf67 100644
--- a/superset-frontend/src/explore/controlPanels/timeGrainSqlaAnimationOverrides.js
+++ b/superset-frontend/src/explore/controlPanels/timeGrainSqlaAnimationOverrides.ts
@@ -16,11 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import type { ControlPanelState, Dataset } from '@superset-ui/chart-controls';
+
+interface TimeGrainOverrideState {
+  choices: [string, string][] | null;
+}
+
 export default {
   default: null,
-  mapStateToProps: state => ({
-    choices: state.datasource
-      ? state.datasource.time_grain_sqla.filter(o => o[0] !== null)
-      : null,
+  mapStateToProps: (state: ControlPanelState): TimeGrainOverrideState => ({
+    choices:
+      state.datasource && 'time_grain_sqla' in state.datasource
+        ? ((state.datasource as Dataset).time_grain_sqla?.filter(
+            (o: [string, string]) => o[0] !== null,
+          ) ?? null)
+        : null,
   }),
 };
diff --git a/superset-frontend/src/types/TagType.ts b/superset-frontend/src/types/TagType.ts
index 04db0c18e6..e7165234a3 100644
--- a/superset-frontend/src/types/TagType.ts
+++ b/superset-frontend/src/types/TagType.ts
@@ -39,5 +39,3 @@ export interface TagType {
   css?: SerializedStyles;
   closable?: boolean;
 }
-
-export default TagType;
diff --git a/superset-frontend/src/utils/DebouncedMessageQueue.test.ts b/superset-frontend/src/utils/DebouncedMessageQueue.test.ts
new file mode 100644
index 0000000000..2631d7c20c
--- /dev/null
+++ b/superset-frontend/src/utils/DebouncedMessageQueue.test.ts
@@ -0,0 +1,66 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import DebouncedMessageQueue from './DebouncedMessageQueue';
+
+describe('DebouncedMessageQueue', () => {
+  it('should create a queue with default options', () => {
+    const queue = new DebouncedMessageQueue();
+    expect(queue).toBeDefined();
+    expect(queue.trigger).toBeInstanceOf(Function);
+  });
+
+  it('should accept custom configuration options', () => {
+    const mockCallback = jest.fn();
+    const queue = new DebouncedMessageQueue({
+      callback: mockCallback,
+      sizeThreshold: 500,
+      delayThreshold: 2000,
+    });
+    expect(queue).toBeDefined();
+  });
+
+  it('should append items to the queue', () => {
+    const mockCallback = jest.fn();
+    const queue = new DebouncedMessageQueue({ callback: mockCallback });
+
+    const testEvent = { id: 1, message: 'test' };
+    queue.append(testEvent);
+
+    // Verify the append method doesn't throw
+    expect(() => queue.append(testEvent)).not.toThrow();
+  });
+
+  it('should handle generic types properly', () => {
+    interface TestEvent {
+      id: number;
+      data: string;
+    }
+
+    const mockCallback = jest.fn();
+    const queue = new DebouncedMessageQueue<TestEvent>({
+      callback: mockCallback,
+    });
+
+    const testEvent: TestEvent = { id: 1, data: 'test' };
+    queue.append(testEvent);
+
+    expect(() => queue.append(testEvent)).not.toThrow();
+  });
+});
diff --git a/superset-frontend/src/utils/DebouncedMessageQueue.js b/superset-frontend/src/utils/DebouncedMessageQueue.ts
similarity index 69%
rename from superset-frontend/src/utils/DebouncedMessageQueue.js
rename to superset-frontend/src/utils/DebouncedMessageQueue.ts
index 6fd6c5b778..99b50650ae 100644
--- a/superset-frontend/src/utils/DebouncedMessageQueue.js
+++ b/superset-frontend/src/utils/DebouncedMessageQueue.ts
@@ -18,26 +18,45 @@
  */
 import { debounce } from 'lodash';
 
-class DebouncedMessageQueue {
+export interface DebouncedMessageQueueOptions<T> {
+  callback?: (events: T[]) => void;
+  sizeThreshold?: number;
+  delayThreshold?: number;
+}
+
+class DebouncedMessageQueue<T = Record<string, unknown>> {
+  private queue: T[];
+
+  private readonly sizeThreshold: number;
+
+  private readonly delayThreshold: number;
+
+  private readonly callback: (events: T[]) => void;
+
+  public readonly trigger: () => void;
+
   constructor({
     callback = () => {},
     sizeThreshold = 1000,
     delayThreshold = 1000,
-  }) {
+  }: DebouncedMessageQueueOptions<T> = {}) {
     this.queue = [];
     this.sizeThreshold = sizeThreshold;
     this.delayThreshold = delayThreshold;
-
-    this.trigger = debounce(this.trigger.bind(this), this.delayThreshold);
     this.callback = callback;
+
+    this.trigger = debounce(
+      this.triggerInternal.bind(this),
+      this.delayThreshold,
+    );
   }
 
-  append(eventData) {
+  append(eventData: T): void {
     this.queue.push(eventData);
     this.trigger();
   }
 
-  trigger() {
+  private triggerInternal(): void {
     if (this.queue.length > 0) {
       const events = this.queue.splice(0, this.sizeThreshold);
       this.callback.call(null, events);
diff --git a/superset-frontend/src/utils/datasourceUtils.js b/superset-frontend/src/utils/datasourceUtils.js
deleted file mode 100644
index 144a3ff88b..0000000000
--- a/superset-frontend/src/utils/datasourceUtils.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-export const getDatasourceAsSaveableDataset = source => ({
-  columns: source.columns,
-  name: source?.datasource_name || source?.name || 'Untitled',
-  dbId: source?.database?.id || source?.dbId,
-  sql: source?.sql || '',
-  catalog: source?.catalog,
-  schema: source?.schema,
-  templateParams: source?.templateParams,
-});
diff --git a/superset-frontend/src/utils/datasourceUtils.test.ts b/superset-frontend/src/utils/datasourceUtils.test.ts
new file mode 100644
index 0000000000..a935a71d7e
--- /dev/null
+++ b/superset-frontend/src/utils/datasourceUtils.test.ts
@@ -0,0 +1,190 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ColumnMeta, Metric } from '@superset-ui/chart-controls';
+import { DatasourceType } from '@superset-ui/core';
+import type { Datasource } from 'src/explore/types';
+import type { QueryEditor } from 'src/SqlLab/types';
+import { getDatasourceAsSaveableDataset } from './datasourceUtils';
+
+const mockColumnMeta: ColumnMeta = {
+  column_name: 'test_column',
+  type: 'VARCHAR',
+  is_dttm: false,
+  verbose_name: 'Test Column',
+  description: 'A test column',
+  expression: '',
+  filterable: true,
+  groupby: true,
+  id: 1,
+  type_generic: 1,
+  python_date_format: null,
+  optionName: 'test_column',
+};
+
+const mockMetric: Metric = {
+  id: 1,
+  uuid: 'metric-1',
+  metric_name: 'count',
+  verbose_name: 'Count',
+  description: 'Count of records',
+  d3format: null,
+  currency: null,
+  warning_text: null,
+  // optionName removed - not part of Metric interface
+};
+
+const mockDatasource: Datasource = {
+  id: 1,
+  type: DatasourceType.Table,
+  columns: [mockColumnMeta],
+  metrics: [mockMetric],
+  column_formats: {},
+  verbose_map: {},
+  main_dttm_col: '',
+  order_by_choices: null,
+  datasource_name: 'Test Datasource',
+  name: 'test_table',
+  catalog: 'test_catalog',
+  schema: 'test_schema',
+  description: 'Test datasource',
+  database: {
+    id: 123,
+    database_name: 'test_db',
+    sqlalchemy_uri: 'postgresql://test',
+  },
+};
+
+const mockQueryEditor: QueryEditor = {
+  id: 'query-1',
+  immutableId: 'immutable-query-1',
+  version: 1,
+  name: 'Test Query',
+  sql: 'SELECT * FROM users',
+  dbId: 456,
+  autorun: false,
+  remoteId: null,
+  catalog: 'prod_catalog',
+  schema: 'public',
+  templateParams: '{"param1": "value1"}',
+};
+
+describe('getDatasourceAsSaveableDataset', () => {
+  test('should convert Datasource object correctly', () => {
+    const result = getDatasourceAsSaveableDataset(mockDatasource);
+
+    expect(result).toEqual({
+      columns: [mockColumnMeta],
+      name: 'Test Datasource',
+      dbId: 123,
+      sql: '',
+      catalog: 'test_catalog',
+      schema: 'test_schema',
+      templateParams: null,
+    });
+  });
+
+  test('should convert QueryEditor object correctly', () => {
+    const queryWithColumns = { ...mockQueryEditor, columns: [mockColumnMeta] };
+    const result = getDatasourceAsSaveableDataset(queryWithColumns);
+
+    expect(result).toEqual({
+      columns: [mockColumnMeta],
+      name: 'Test Query',
+      dbId: 456,
+      sql: 'SELECT * FROM users',
+      catalog: 'prod_catalog',
+      schema: 'public',
+      templateParams: '{"param1": "value1"}',
+    });
+  });
+
+  test('should handle datasource with fallback name from name property', () => {
+    const datasourceWithoutDatasourceName: Datasource = {
+      ...mockDatasource,
+      datasource_name: null,
+      name: 'fallback_name',
+    };
+
+    const result = getDatasourceAsSaveableDataset(
+      datasourceWithoutDatasourceName,
+    );
+
+    expect(result.name).toBe('fallback_name');
+  });
+
+  test('should use "Untitled" as fallback when no name is available', () => {
+    const datasourceWithoutName: Datasource = {
+      ...mockDatasource,
+      datasource_name: null,
+      name: '',
+    };
+
+    const result = getDatasourceAsSaveableDataset(datasourceWithoutName);
+
+    expect(result.name).toBe('Untitled');
+  });
+
+  test('should handle missing database object', () => {
+    const datasourceWithoutDatabase: Datasource = {
+      ...mockDatasource,
+      database: undefined,
+    };
+
+    const result = getDatasourceAsSaveableDataset(datasourceWithoutDatabase);
+
+    expect(result.dbId).toBe(0);
+  });
+
+  test('should handle QueryEditor with missing dbId', () => {
+    const queryEditorWithoutDbId: QueryEditor = {
+      ...mockQueryEditor,
+      dbId: undefined,
+    };
+
+    const result = getDatasourceAsSaveableDataset(queryEditorWithoutDbId);
+
+    expect(result.dbId).toBe(0);
+  });
+
+  test('should handle QueryEditor without sql property', () => {
+    const queryEditorWithoutSql: QueryEditor = {
+      ...mockQueryEditor,
+      sql: '',
+    };
+
+    const result = getDatasourceAsSaveableDataset(queryEditorWithoutSql);
+
+    expect(result.sql).toBe('');
+  });
+
+  test('should handle null values for optional properties', () => {
+    const minimalQueryEditor: QueryEditor = {
+      ...mockQueryEditor,
+      catalog: null,
+      schema: undefined,
+      templateParams: '',
+    };
+
+    const result = getDatasourceAsSaveableDataset(minimalQueryEditor);
+
+    expect(result.catalog).toBe(null);
+    expect(result.schema).toBe(null);
+    expect(result.templateParams).toBe(null);
+  });
+});
diff --git a/superset-frontend/src/utils/datasourceUtils.ts b/superset-frontend/src/utils/datasourceUtils.ts
new file mode 100644
index 0000000000..3f395f9a82
--- /dev/null
+++ b/superset-frontend/src/utils/datasourceUtils.ts
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { ColumnMeta } from '@superset-ui/chart-controls';
+import type { ISaveableDatasource } from 'src/SqlLab/components/SaveDatasetModal';
+
+// Flexible interface that captures what this function actually needs to work
+// This allows it to accept various datasource-like objects from different parts of the codebase
+interface DatasourceInput {
+  // Common properties that all datasource-like objects should have
+  name?: string | null; // Allow null for compatibility
+
+  // Optional properties that may exist on different datasource variants
+  datasource_name?: string | null; // Allow null for compatibility
+  columns?: any[]; // Can be ColumnMeta[], DatasourcePanelColumn[], ISimpleColumn[], etc.
+  database?: { id?: number };
+  dbId?: number;
+  sql?: string | null; // Allow null for compatibility
+  catalog?: string | null;
+  schema?: string | null;
+  templateParams?: string;
+
+  // Type discriminator for QueryEditor-like objects
+  version?: number;
+}
+
+export const getDatasourceAsSaveableDataset = (
+  source: DatasourceInput,
+): ISaveableDatasource => {
+  // Type guard: QueryEditor-like objects have version property
+  const isQueryEditorLike = typeof source.version === 'number';
+
+  return {
+    columns: (source.columns as ColumnMeta[]) || [],
+    name: source.datasource_name || source.name || 'Untitled',
+    dbId: source.database?.id || source.dbId || 0,
+    sql: source.sql || '',
+    catalog: source.catalog || null,
+    schema: source.schema || null,
+    templateParams: isQueryEditorLike ? source.templateParams || null : null,
+  };
+};
diff --git a/superset-frontend/src/utils/getControlsForVizType.test.ts b/superset-frontend/src/utils/getControlsForVizType.test.ts
new file mode 100644
index 0000000000..cf94610751
--- /dev/null
+++ b/superset-frontend/src/utils/getControlsForVizType.test.ts
@@ -0,0 +1,101 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { getChartControlPanelRegistry, JsonObject } from '@superset-ui/core';
+import getControlsForVizType from 'src/utils/getControlsForVizType';
+
+const fakePluginControls: JsonObject = {
+  controlPanelSections: [
+    {
+      label: 'Fake Control Panel Sections',
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'y_axis_bounds',
+            config: {
+              type: 'BoundsControl',
+              label: 'Value bounds',
+              default: [null, null],
+              description: 'Value bounds for the y axis',
+            },
+          },
+        ],
+        [
+          {
+            name: 'adhoc_filters',
+            config: {
+              type: 'AdhocFilterControl',
+              label: 'Fake Filters',
+              default: null,
+            },
+          },
+        ],
+      ],
+    },
+    {
+      label: 'Fake Control Panel Sections 2',
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'column_collection',
+            config: {
+              type: 'CollectionControl',
+              label: 'Fake Collection Control',
+            },
+          },
+        ],
+      ],
+    },
+  ],
+};
+
+describe('getControlsForVizType', () => {
+  beforeEach(() => {
+    getChartControlPanelRegistry().registerValue(
+      'chart_controls_inventory_fake',
+      fakePluginControls,
+    );
+  });
+
+  it('returns a map of the controls', () => {
+    expect(
+      JSON.stringify(getControlsForVizType('chart_controls_inventory_fake')),
+    ).toEqual(
+      JSON.stringify({
+        y_axis_bounds: {
+          type: 'BoundsControl',
+          label: 'Value bounds',
+          default: [null, null],
+          description: 'Value bounds for the y axis',
+        },
+        adhoc_filters: {
+          type: 'AdhocFilterControl',
+          label: 'Fake Filters',
+          default: null,
+        },
+        column_collection: {
+          type: 'CollectionControl',
+          label: 'Fake Collection Control',
+        },
+      }),
+    );
+  });
+});
diff --git a/superset-frontend/src/utils/getControlsForVizType.ts b/superset-frontend/src/utils/getControlsForVizType.ts
new file mode 100644
index 0000000000..4fff9a06f1
--- /dev/null
+++ b/superset-frontend/src/utils/getControlsForVizType.ts
@@ -0,0 +1,74 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import memoizeOne from 'memoize-one';
+import { isControlPanelSectionConfig } from '@superset-ui/chart-controls';
+import { getChartControlPanelRegistry, JsonObject } from '@superset-ui/core';
+import type { ControlMap } from 'src/components/AlteredSliceTag/types';
+import { controls } from '../explore/controls';
+
+const memoizedControls = memoizeOne(
+  (vizType: string, controlPanel: JsonObject | undefined): ControlMap => {
+    const controlsMap: ControlMap = {};
+    if (!controlPanel) return controlsMap;
+
+    const sections = controlPanel.controlPanelSections || [];
+    (Array.isArray(sections) ? sections : [])
+      .filter(isControlPanelSectionConfig)
+      .forEach(section => {
+        if (section.controlSetRows && Array.isArray(section.controlSetRows)) {
+          section.controlSetRows.forEach(row => {
+            if (Array.isArray(row)) {
+              row.forEach(control => {
+                if (!control) return;
+                if (typeof control === 'string') {
+                  // For now, we have to look in controls.jsx to get the config for some controls.
+                  // Once everything is migrated out, delete this if statement.
+                  const controlConfig = (controls as any)[control];
+                  if (controlConfig) {
+                    controlsMap[control] = controlConfig;
+                  }
+                } else if (
+                  typeof control === 'object' &&
+                  control &&
+                  'name' in control &&
+                  'config' in control
+                ) {
+                  // condition needed because there are elements, e.g. <hr /> in some control configs (I'm looking at you, FilterBox!)
+                  const controlObj = control as {
+                    name: string;
+                    config: JsonObject;
+                  };
+                  controlsMap[controlObj.name] = controlObj.config;
+                }
+              });
+            }
+          });
+        }
+      });
+    return controlsMap;
+  },
+);
+
+const getControlsForVizType = (vizType: string): ControlMap => {
+  const controlPanel = getChartControlPanelRegistry().get(vizType);
+  return memoizedControls(vizType, controlPanel);
+};
+
+export default getControlsForVizType;
diff --git a/superset-frontend/src/utils/hostNamesConfig.test.ts b/superset-frontend/src/utils/hostNamesConfig.test.ts
new file mode 100644
index 0000000000..dcfcf3e2a5
--- /dev/null
+++ b/superset-frontend/src/utils/hostNamesConfig.test.ts
@@ -0,0 +1,58 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { availableDomains, allowCrossDomain } from './hostNamesConfig';
+
+describe('hostNamesConfig', () => {
+  beforeEach(() => {
+    // Reset DOM
+    document.body.innerHTML = '';
+
+    // Mock window.location
+    Object.defineProperty(window, 'location', {
+      value: {
+        hostname: 'localhost',
+        search: '',
+      },
+      writable: true,
+    });
+  });
+
+  test('should export availableDomains as array of strings', () => {
+    expect(Array.isArray(availableDomains)).toBe(true);
+    availableDomains.forEach(domain => {
+      expect(typeof domain).toBe('string');
+    });
+  });
+
+  test('should export allowCrossDomain as boolean', () => {
+    expect(typeof allowCrossDomain).toBe('boolean');
+  });
+
+  test('should determine allowCrossDomain based on availableDomains length', () => {
+    const expectedValue = availableDomains.length > 1;
+    expect(allowCrossDomain).toBe(expectedValue);
+  });
+
+  test('availableDomains should contain at least the current hostname', () => {
+    // Since we're testing the already computed values, we check they contain localhost
+    // or the configuration returns empty array if app container is missing
+    expect(availableDomains.length >= 0).toBe(true);
+  });
+});
diff --git a/superset-frontend/src/utils/hostNamesConfig.js b/superset-frontend/src/utils/hostNamesConfig.ts
similarity index 85%
rename from superset-frontend/src/utils/hostNamesConfig.js
rename to superset-frontend/src/utils/hostNamesConfig.ts
index 2ba9b7555c..354e8c0fe5 100644
--- a/superset-frontend/src/utils/hostNamesConfig.js
+++ b/superset-frontend/src/utils/hostNamesConfig.ts
@@ -19,7 +19,7 @@
 import { initFeatureFlags } from '@superset-ui/core';
 import getBootstrapData from './getBootstrapData';
 
-function getDomainsConfig() {
+function getDomainsConfig(): string[] {
   const appContainer = document.getElementById('app');
   if (!appContainer) {
     return [];
@@ -42,13 +42,15 @@ function getDomainsConfig() {
   initFeatureFlags(bootstrapData.common.feature_flags);
 
   if (bootstrapData?.common?.conf?.SUPERSET_WEBSERVER_DOMAINS) {
-    bootstrapData.common.conf.SUPERSET_WEBSERVER_DOMAINS.forEach(hostName => {
+    const domains = bootstrapData.common.conf
+      .SUPERSET_WEBSERVER_DOMAINS as string[];
+    domains.forEach((hostName: string) => {
       availableDomains.add(hostName);
     });
   }
   return Array.from(availableDomains);
 }
 
-export const availableDomains = getDomainsConfig();
+export const availableDomains: string[] = getDomainsConfig();
 
-export const allowCrossDomain = availableDomains.length > 1;
+export const allowCrossDomain: boolean = availableDomains.length > 1;
diff --git a/superset-frontend/src/utils/reducerUtils.test.ts b/superset-frontend/src/utils/reducerUtils.test.ts
new file mode 100644
index 0000000000..a342ff274f
--- /dev/null
+++ b/superset-frontend/src/utils/reducerUtils.test.ts
@@ -0,0 +1,129 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  addToObject,
+  alterInObject,
+  alterInArr,
+  removeFromArr,
+  addToArr,
+} from './reducerUtils';
+
+interface TestItem {
+  id?: string;
+  name: string;
+  value: number;
+}
+
+const mockState = {
+  objects: {
+    'item-1': { id: 'item-1', name: 'Item 1', value: 10 },
+    'item-2': { id: 'item-2', name: 'Item 2', value: 20 },
+  },
+  items: [
+    { id: 'item-1', name: 'Item 1', value: 10 },
+    { id: 'item-2', name: 'Item 2', value: 20 },
+  ],
+};
+
+test('addToObject adds new object to state with generated id', () => {
+  const newItem: TestItem = { name: 'New Item', value: 30 };
+  const result = addToObject(mockState, 'objects', newItem);
+
+  expect(result).not.toBe(mockState);
+  expect(result.objects).not.toBe(mockState.objects);
+  expect(Object.keys(result.objects)).toHaveLength(3);
+
+  const addedItems = Object.values(result.objects).filter(
+    item => (item as TestItem).name === 'New Item',
+  );
+  expect(addedItems).toHaveLength(1);
+  expect((addedItems[0] as TestItem).id).toBeTruthy();
+});
+
+test('addToObject adds new object with existing id', () => {
+  const newItem: TestItem = { id: 'item-3', name: 'Item 3', value: 30 };
+  const result = addToObject(mockState, 'objects', newItem);
+
+  expect(result.objects['item-3']).toEqual(newItem);
+});
+
+test('alterInObject modifies existing object', () => {
+  const targetItem: TestItem = { id: 'item-1', name: 'Item 1', value: 10 };
+  const alterations = { value: 15 };
+  const result = alterInObject(mockState, 'objects', targetItem, alterations);
+
+  expect(result.objects['item-1'].value).toBe(15);
+  expect(result.objects['item-1'].name).toBe('Item 1');
+  expect(result.objects['item-2']).toBe(mockState.objects['item-2']);
+});
+
+test('alterInArr modifies existing array item', () => {
+  const targetItem: TestItem = { id: 'item-1', name: 'Item 1', value: 10 };
+  const alterations = { value: 15 };
+  const result = alterInArr(mockState, 'items', targetItem, alterations);
+
+  expect(result.items[0].value).toBe(15);
+  expect(result.items[0].name).toBe('Item 1');
+  expect(result.items[1]).toBe(mockState.items[1]);
+});
+
+test('removeFromArr removes item from array', () => {
+  const targetItem: TestItem = { id: 'item-1', name: 'Item 1', value: 10 };
+  const result = removeFromArr(mockState, 'items', targetItem);
+
+  expect(result.items).toHaveLength(1);
+  expect(result.items[0].id).toBe('item-2');
+});
+
+test('removeFromArr with custom idKey', () => {
+  const stateWithCustomKey = {
+    items: [
+      { customId: 'a', name: 'Item A' },
+      { customId: 'b', name: 'Item B' },
+    ],
+  };
+  const targetItem = { customId: 'a', name: 'Item A' };
+  const result = removeFromArr(
+    stateWithCustomKey,
+    'items',
+    targetItem,
+    'customId',
+  );
+
+  expect(result.items).toHaveLength(1);
+  expect(result.items[0].customId).toBe('b');
+});
+
+test('addToArr adds new item to array with generated id', () => {
+  const newItem: TestItem = { name: 'New Item', value: 30 };
+  const result = addToArr(mockState, 'items', newItem);
+
+  expect(result.items).toHaveLength(3);
+  expect(result.items[2].name).toBe('New Item');
+  expect(result.items[2].id).toBeTruthy();
+});
+
+test('addToArr adds new item with existing id', () => {
+  const newItem: TestItem = { id: 'item-3', name: 'Item 3', value: 30 };
+  const result = addToArr(mockState, 'items', newItem);
+
+  expect(result.items).toHaveLength(3);
+  expect(result.items[2]).toEqual(newItem);
+});
diff --git a/superset-frontend/src/utils/reducerUtils.ts b/superset-frontend/src/utils/reducerUtils.ts
new file mode 100644
index 0000000000..9a8eb9d4c0
--- /dev/null
+++ b/superset-frontend/src/utils/reducerUtils.ts
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { nanoid } from 'nanoid';
+
+interface ObjectWithId {
+  id?: string;
+  [key: string]: any;
+}
+
+interface StateWithObject {
+  [key: string]: { [id: string]: ObjectWithId } | any;
+}
+
+interface StateWithArray {
+  [key: string]: ObjectWithId[] | any;
+}
+
+export function addToObject<T extends ObjectWithId>(
+  state: StateWithObject,
+  arrKey: string,
+  obj: T,
+): StateWithObject {
+  const newObject = { ...state[arrKey] };
+  const copiedObject = { ...obj };
+
+  if (!copiedObject.id) {
+    copiedObject.id = nanoid();
+  }
+  newObject[copiedObject.id] = copiedObject;
+  return { ...state, [arrKey]: newObject };
+}
+
+export function alterInObject<T extends ObjectWithId>(
+  state: StateWithObject,
+  arrKey: string,
+  obj: T,
+  alterations: Partial<T>,
+): StateWithObject {
+  const newObject = { ...state[arrKey] };
+  newObject[obj.id!] = { ...newObject[obj.id!], ...alterations };
+  return { ...state, [arrKey]: newObject };
+}
+
+export function alterInArr<T extends ObjectWithId>(
+  state: StateWithArray,
+  arrKey: string,
+  obj: T,
+  alterations: Partial<T>,
+): StateWithArray {
+  // Finds an item in an array in the state and replaces it with a
+  // new object with an altered property
+  const idKey = 'id';
+  const newArr: T[] = [];
+  state[arrKey].forEach((arrItem: T) => {
+    if (obj[idKey] === arrItem[idKey]) {
+      newArr.push({ ...arrItem, ...alterations });
+    } else {
+      newArr.push(arrItem);
+    }
+  });
+  return { ...state, [arrKey]: newArr };
+}
+
+export function removeFromArr<T extends ObjectWithId>(
+  state: StateWithArray,
+  arrKey: string,
+  obj: T,
+  idKey = 'id',
+): StateWithArray {
+  const newArr: T[] = [];
+  state[arrKey].forEach((arrItem: T) => {
+    if (!(obj[idKey as keyof T] === arrItem[idKey as keyof T])) {
+      newArr.push(arrItem);
+    }
+  });
+  return { ...state, [arrKey]: newArr };
+}
+
+export function addToArr<T extends ObjectWithId>(
+  state: StateWithArray,
+  arrKey: string,
+  obj: T,
+): StateWithArray {
+  const newObj = { ...obj };
+  if (!newObj.id) {
+    newObj.id = nanoid();
+  }
+  const newState: { [key: string]: T[] } = {};
+  newState[arrKey] = [...state[arrKey], newObj];
+  return { ...state, ...newState };
+}