fix(types): improve type safety in ExploreViewContainer

- Create CombinedExploreActions type for combined action creators
- Add BoundActions<T> utility type for bound action creators
- Update ControlPanelsContainer to use Pick<ExploreActions, 'setControlValue'>
- Change datasource_type from string to DatasourceType enum
- Change exploreState to use ExplorePageState['explore']
- Replace control.label() as any with as unknown as ControlPanelState
- Add eslint-disable comments for remaining necessary casts
- Remove unnecessary as any casts from connect() and SaveModal

Reduces 'as any' casts from ~12 to 5, with remaining casts
documented with eslint-disable comments explaining why they're needed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-01-19 14:35:40 -08:00
parent 5fb917e07f
commit c27bf8da53
2 changed files with 62 additions and 21 deletions

View File

@@ -17,8 +17,15 @@
* under the License.
*/
/* eslint camelcase: 0 */
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { bindActionCreators, Dispatch } from 'redux';
import {
ComponentType,
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { bindActionCreators, Dispatch, ActionCreatorsMapObject } from 'redux';
import { connect } from 'react-redux';
import {
useChangeEffect,
@@ -28,8 +35,12 @@ import {
QueryFormData,
JsonObject,
MatrixifyFormData,
DatasourceType,
} from '@superset-ui/core';
import { ControlStateMapping } from '@superset-ui/chart-controls';
import {
ControlStateMapping,
ControlPanelState,
} from '@superset-ui/chart-controls';
import { t, styled, css, useTheme } from '@apache-superset/core/ui';
import { logging } from '@apache-superset/core';
import { debounce, isEqual, isObjectLike, omit, pick } from 'lodash';
@@ -62,6 +73,7 @@ import { datasourcesActions } from 'src/explore/actions/datasourcesActions';
import { mountExploreUrl } from 'src/explore/exploreUtils';
import { getFormDataFromControls } from 'src/explore/controlUtils';
import * as exploreActions from 'src/explore/actions/exploreActions';
import { ExploreActions } from 'src/explore/actions/exploreActions';
import * as saveModalActions from 'src/explore/actions/saveModalActions';
import { useTabId } from 'src/hooks/useTabId';
import withToasts from 'src/components/MessageToasts/withToasts';
@@ -69,6 +81,7 @@ import {
ChartState,
Datasource,
ExplorePageInitialData,
ExplorePageState,
SaveActionType,
} from 'src/explore/types';
import { Slice } from 'src/types/Chart';
@@ -311,7 +324,7 @@ interface OwnProps {
interface StateProps {
isDatasourceMetaLoading: boolean;
datasource: Datasource;
datasource_type: string;
datasource_type: DatasourceType;
datasourceId: number;
dashboardId?: number;
colorScheme?: string;
@@ -337,19 +350,29 @@ interface StateProps {
ownState?: JsonObject;
impressionId: string;
user: User;
exploreState: ExploreRootState['explore'];
exploreState: ExplorePageState['explore'];
reports: JsonObject;
metadata?: ExplorePageInitialData['metadata'];
saveAction?: SaveActionType | null;
isSaveModalVisible: boolean;
}
// Combined actions type representing all action creators used in Explore
type CombinedExploreActions = typeof exploreActions &
typeof datasourcesActions &
typeof saveModalActions &
typeof chartActions &
typeof logActions;
// Bound action creators type (what mapDispatchToProps actually returns)
type BoundActions<T extends ActionCreatorsMapObject> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => R extends (...args: unknown[]) => infer R2 ? R2 : R
: T[K];
};
interface DispatchProps {
actions: typeof exploreActions &
typeof datasourcesActions &
typeof saveModalActions &
typeof chartActions &
typeof logActions;
actions: BoundActions<CombinedExploreActions>;
}
type ExploreViewContainerProps = StateProps & DispatchProps & OwnProps;
@@ -744,7 +767,10 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
.filter(control => control.validationErrors?.includes(message))
.map(control =>
typeof control.label === 'function'
? control.label(props.exploreState as any, control)
? control.label(
props.exploreState as unknown as ControlPanelState,
control,
)
: control.label,
);
return [matchingLabels, message];
@@ -787,7 +813,10 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
.filter(control => control.validationErrors?.includes(message))
.map(control =>
typeof control.label === 'function'
? control.label(props.exploreState as any, control)
? control.label(
props.exploreState as unknown as ControlPanelState,
control,
)
: control.label,
);
return [matchingLabels, message];
@@ -858,6 +887,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
return (
<ExploreContainer>
<ConnectedExploreChartHeader
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Combined actions type is compatible at runtime
actions={props.actions as any}
canOverwrite={props.can_overwrite}
canDownload={props.can_download}
@@ -934,6 +964,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
/>
</span>
</div>
{/* eslint-disable @typescript-eslint/no-explicit-any -- DataSourcePanel uses narrower types that are compatible at runtime */}
<DataSourcePanel
formData={props.form_data}
datasource={props.datasource as any}
@@ -941,6 +972,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
actions={props.actions as any}
width={width}
/>
{/* eslint-enable @typescript-eslint/no-explicit-any */}
</Resizable>
{isCollapsed ? (
<div
@@ -978,12 +1010,13 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
className="col-sm-3 explore-column controls-column"
>
<ConnectedControlPanelsContainer
exploreState={props.exploreState as any}
exploreState={props.exploreState}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Combined actions type is compatible at runtime
actions={props.actions as any}
form_data={props.form_data}
controls={props.controls}
chart={props.chart}
datasource_type={props.datasource_type as any}
datasource_type={props.datasource_type}
isDatasourceMetaLoading={props.isDatasourceMetaLoading}
onQuery={onQuery}
onStop={onStop}
@@ -1005,7 +1038,7 @@ function ExploreViewContainer(props: ExploreViewContainerProps) {
{props.isSaveModalVisible && (
<SaveModal
addDangerToast={props.addDangerToast}
actions={props.actions as any}
actions={props.actions}
form_data={props.form_data}
sliceName={props.sliceName ?? undefined}
dashboardId={props.dashboardId ?? null}
@@ -1161,7 +1194,9 @@ function mapStateToProps(state: ExploreRootState) {
ownState: dataMask[slice_id]?.ownState,
impressionId,
user,
exploreState: explore,
// ExploreRootState['explore'] is compatible with ExplorePageState['explore']
// but has additional optional fields; casting is safe here
exploreState: explore as unknown as ExplorePageState['explore'],
reports,
metadata,
saveAction: explore.saveAction,
@@ -1169,8 +1204,8 @@ function mapStateToProps(state: ExploreRootState) {
};
}
function mapDispatchToProps(dispatch: Dispatch) {
const actions = {
function mapDispatchToProps(dispatch: Dispatch): DispatchProps {
const actions: CombinedExploreActions = {
...exploreActions,
...datasourcesActions,
...saveModalActions,
@@ -1178,11 +1213,16 @@ function mapDispatchToProps(dispatch: Dispatch) {
...logActions,
};
return {
actions: bindActionCreators(actions as any, dispatch),
actions: bindActionCreators(
actions as ActionCreatorsMapObject,
dispatch,
) as BoundActions<CombinedExploreActions>,
};
}
// withToasts HOC expects ComponentType<any>, requiring type assertion
// The connected component properly handles StateProps & DispatchProps & OwnProps
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withToasts(memo(ExploreViewContainer)) as any);
)(withToasts(memo(ExploreViewContainer)) as ComponentType<OwnProps>);