From 9b5d89bfaae83b2f0d1c18af4134bf4295ded98c Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Tue, 21 Apr 2026 18:37:04 -0700 Subject: [PATCH] chore(superset-ui-core): forward-compat fixes for TypeScript 6.0 Scoped to packages/superset-ui-core. All changes compile cleanly on TypeScript 5.4.5 (current CI) and eliminate TS 6.0 errors in this package. Key fixes: - forwardRef callbacks use ForwardedRef instead of RefObject (AsyncEsmComponent, DropdownContainer, ModalTrigger). ModalTrigger also narrows object-ref access with a typeof-function guard. - StatefulChart: cast onRenderFailure to the shared HandlerFunction type so the stricter 6.0 inference matches the SuperChart prop. - AntdEnhanced: drop the wider IconType.component from the spread so {...rest} can no longer override the explicit binding under 6.0's stricter JSX attribute checking. - Select / AsyncSelect: widen antd sorter/filter/handler call-sites with targeted `as unknown as ...` casts against antd's BaseOptionType / DefaultOptionType / SelectHandler shapes. Behaviour unchanged; only the type boundary moves. - VirtualTable: accept width?: number from react-resize-detector's 6.0 callback signature, defaulting missing width to 0. - TableCollection: cast columns / rowClassName / onChange to antd's generic ColumnsType and TableProps variants. - TimezoneSelector: cast our comparator to antd's LabeledValue-based sorter signature; our comparator only reads fields that always exist on TimezoneOption, so the broader shape is safe at runtime. - connection/types: widen FetchRetryOptions retryDelay/retryOn to accept `Error | null` / `Response | null`, matching fetch-retry. - fetchTimeRange: rename catch variable and narrow it via the getClientErrorObject parameter type, handling TS 6.0's `unknown` default for caught values. - lruCache: guard Map iterator .value before Map#delete (TS 6.0 types IteratorResult.value as `string | undefined`). - InteractiveTableUtils: initialise columnRef to null for strict property initialization. - types/assets.d.ts: add `declare module '*.css'` for CSS side-effect imports under 6.0's stricter module resolution. Part of the TypeScript 5.4 -> 6.0 migration, split per-package to keep reviews small. No runtime behaviour changes. Co-Authored-By: Claude Opus 4.7 --- .../src/chart/components/StatefulChart.tsx | 3 +- .../components/AsyncEsmComponent/index.tsx | 4 +- .../DropdownContainer/DropdownContainer.tsx | 4 +- .../src/components/Icons/AntdEnhanced.tsx | 10 ++- .../src/components/ModalTrigger/index.tsx | 12 ++- .../src/components/Select/AsyncSelect.tsx | 74 +++++++++++++++---- .../src/components/Select/Select.tsx | 62 ++++++++++++++-- .../src/components/Table/VirtualTable.tsx | 4 +- .../Table/utils/InteractiveTableUtils.ts | 2 +- .../src/components/TableCollection/index.tsx | 18 ++++- .../src/components/TimezoneSelector/index.tsx | 12 ++- .../superset-ui-core/src/connection/types.ts | 12 ++- .../src/time-comparison/fetchTimeRange.ts | 10 ++- .../superset-ui-core/src/utils/lruCache.ts | 7 +- .../superset-ui-core/types/assets.d.ts | 1 + 15 files changed, 190 insertions(+), 45 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx index 2e9a4903410..774bad741c4 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx @@ -28,6 +28,7 @@ import { RequestConfig, getClientErrorObject, } from '../..'; +import type { HandlerFunction } from '../types/Base'; import { Loading } from '../../components/Loading'; import ChartClient from '../clients/ChartClient'; import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton'; @@ -482,7 +483,7 @@ export default function StatefulChart(props: StatefulChartProps) { enableNoResults={enableNoResults} noResults={NoDataComponent && } onRenderSuccess={onRenderSuccess} - onRenderFailure={onRenderFailure} + onRenderFailure={onRenderFailure as HandlerFunction | undefined} hooks={hooks} /> ); diff --git a/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx index aeb8f8b7220..65e833e091a 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx @@ -19,7 +19,7 @@ import { useEffect, useState, - RefObject, + ForwardedRef, forwardRef, ComponentType, ForwardRefExoticComponent, @@ -101,7 +101,7 @@ export function AsyncEsmComponent< const AsyncComponent: AsyncComponent = forwardRef(function AsyncComponent( props: FullProps, - ref: RefObject>, + ref: ForwardedRef>, ) { const [loaded, setLoaded] = useState(component !== undefined); useEffect(() => { diff --git a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.tsx b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.tsx index e8deaf8e120..c334932b2f9 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/DropdownContainer/DropdownContainer.tsx @@ -19,7 +19,7 @@ import { cloneElement, forwardRef, - RefObject, + ForwardedRef, useEffect, useImperativeHandle, useLayoutEffect, @@ -54,7 +54,7 @@ export const DropdownContainer = forwardRef( forceRender, style, }: DropdownContainerProps, - outerRef: RefObject, + outerRef: ForwardedRef, ) => { const theme = useTheme(); const { ref, width = 0 } = useResizeDetector(); diff --git a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx index 947a5ff3926..52cc3f61aae 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Icons/AntdEnhanced.tsx @@ -326,11 +326,17 @@ export const antdEnhancedIcons: Record< .filter(key => !EXCLUDED_ICONS.some(excluded => key.includes(excluded))) .reduce( (acc, key) => { - acc[key as AntdIconNames] = (props: IconType) => ( + acc[key as AntdIconNames] = ({ + // Forward-compat: TS 6.0 treats IconComponentProps.component as a + // different shape than BaseIconProps.component; strip it from spread + // props so our own component binding is authoritative. + component: _ignoredComponent, + ...rest + }: IconType) => ( ); return acc; diff --git a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx index 55339f45170..5359772febd 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/ModalTrigger/index.tsx @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { forwardRef, useState, ReactNode, MouseEvent } from 'react'; +import { + forwardRef, + ForwardedRef, + useState, + ReactNode, + MouseEvent, +} from 'react'; import { Button } from '../Button'; import { Modal } from '../Modal'; @@ -51,7 +57,7 @@ export interface ModalTriggerRef { } export const ModalTrigger = forwardRef( - (props: ModalTriggerProps, ref: ModalTriggerRef | null) => { + (props: ModalTriggerProps, ref: ForwardedRef) => { const [showModal, setShowModal] = useState(false); const { beforeOpen = () => {}, @@ -84,7 +90,7 @@ export const ModalTrigger = forwardRef( setShowModal(true); }; - if (ref) { + if (ref && typeof ref !== 'function') { ref.current = { close, open, showModal }; // eslint-disable-line } diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx index 9acd2f4893f..34f9d099c3f 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx @@ -18,6 +18,7 @@ */ import { forwardRef, + ForwardedRef, FocusEvent, ReactElement, RefObject, @@ -38,6 +39,8 @@ import { getClientErrorObject, } from '@superset-ui/core'; import { + BaseOptionType, + DefaultOptionType, LabeledValue as AntdLabeledValue, RefSelectProps, } from 'antd/es/select'; @@ -146,7 +149,7 @@ const AsyncSelect = forwardRef( maxTagCount: propsMaxTagCount, ...props }: AsyncSelectProps, - ref: RefObject, + ref: ForwardedRef, ) => { const isSingleMode = mode === 'single'; const [selectValue, setSelectValue] = useState(value); @@ -307,7 +310,14 @@ const AsyncSelect = forwardRef( mergedData = prevOptions .filter(previousOption => !dataValues.has(previousOption.value)) .concat(data) - .sort(sortComparatorForNoSearch); + // Forward-compat: TS 6.0 infers stricter antd option types; widen + // the comparator to accept the broader DefaultOptionType shape. + .sort( + sortComparatorForNoSearch as unknown as ( + a: BaseOptionType | DefaultOptionType, + b: BaseOptionType | DefaultOptionType, + ) => number, + ); return mergedData; }); } @@ -435,7 +445,13 @@ const AsyncSelect = forwardRef( if (isDropdownVisible && !inputValue && selectOptions.length > 1) { const sortedOptions = selectOptions .slice() - .sort(sortComparatorForNoSearch); + // Forward-compat: see note in mergeData above. + .sort( + sortComparatorForNoSearch as unknown as ( + a: BaseOptionType | DefaultOptionType, + b: BaseOptionType | DefaultOptionType, + ) => number, + ); if (!isEqual(sortedOptions, selectOptions)) { setSelectOptions(sortedOptions); } @@ -533,14 +549,16 @@ const AsyncSelect = forwardRef( const clearCache = () => fetchedQueries.current.clear(); - useImperativeHandle( - ref, - () => ({ - ...(ref.current as RefSelectProps), + useImperativeHandle(ref, () => { + const current = + ref && typeof ref !== 'function' && ref.current + ? (ref.current as RefSelectProps) + : ({} as RefSelectProps); + return { + ...current, clearCache, - }), - [ref], - ); + }; + }, [ref]); const getPastedTextValue = useCallback( async (text: string) => { @@ -606,8 +624,21 @@ const AsyncSelect = forwardRef( data-test={ariaLabel || name} autoClearSearchValue={autoClearSearchValue} popupRender={popupRender} - filterOption={handleFilterOption} - filterSort={sortComparatorWithSearch} + // Forward-compat: TS 6.0 infers stricter antd option types; local + // helpers typed against AntdLabeledValue are behaviorally compatible + // with the broader BaseOptionType/DefaultOptionType antd expects. + filterOption={ + handleFilterOption as unknown as ( + search: string, + option?: BaseOptionType | DefaultOptionType, + ) => boolean + } + filterSort={ + sortComparatorWithSearch as unknown as ( + a: BaseOptionType | DefaultOptionType, + b: BaseOptionType | DefaultOptionType, + ) => number + } getPopupContainer={ getPopupContainer || (triggerNode => triggerNode.parentNode) } @@ -617,13 +648,26 @@ const AsyncSelect = forwardRef( mode={mappedMode} notFoundContent={isLoading ? t('Loading...') : notFoundContent} onBlur={handleOnBlur} - onDeselect={handleOnDeselect} + // Forward-compat: TS 6.0 narrows the Select value type handed to + // SelectHandler; our local handlers already accept the broader union. + onDeselect={ + handleOnDeselect as unknown as ( + value: unknown, + option: BaseOptionType | DefaultOptionType, + ) => void + } onOpenChange={handleOnDropdownVisibleChange} - // @ts-expect-error + // @ts-expect-error antd Select does not declare onPaste on its prop + // surface, but the underlying input accepts it and we rely on that. onPaste={onPaste} onPopupScroll={handlePagination} onSearch={showSearch ? handleOnSearch : undefined} - onSelect={handleOnSelect} + onSelect={ + handleOnSelect as unknown as ( + value: unknown, + option: BaseOptionType | DefaultOptionType, + ) => void + } onClear={handleClear} options={fullSelectOptions} optionRender={option => {option.label || option.value}} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx index ead92724684..d71bbc64277 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx @@ -34,6 +34,8 @@ import { t } from '@apache-superset/core/translation'; import { ensureIsArray, formatNumber, usePrevious } from '@superset-ui/core'; import { Constants } from '@superset-ui/core/components'; import { + BaseOptionType, + DefaultOptionType, LabeledValue as AntdLabeledValue, RefSelectProps, } from 'antd/es/select'; @@ -211,7 +213,17 @@ const Select = forwardRef( ); const initialOptionsSorted = useMemo( - () => initialOptions.slice().sort(sortSelectedFirst), + () => + initialOptions + .slice() + // Forward-compat: TS 6.0 infers stricter antd option types; widen the + // comparator to accept the broader DefaultOptionType shape. + .sort( + sortSelectedFirst as unknown as ( + a: BaseOptionType | DefaultOptionType, + b: BaseOptionType | DefaultOptionType, + ) => number, + ), [initialOptions, sortSelectedFirst], ); @@ -239,7 +251,17 @@ const Select = forwardRef( missingValues.length > 0 ? missingValues.concat(selectOptions) : selectOptions; - return result.slice().sort(sortSelectedFirst); + return ( + result + .slice() + // Forward-compat: see note on initialOptionsSorted. + .sort( + sortSelectedFirst as unknown as ( + a: BaseOptionType | DefaultOptionType, + b: BaseOptionType | DefaultOptionType, + ) => number, + ) + ); }, [selectOptions, selectValue, sortSelectedFirst]); const enabledOptions = useMemo( @@ -751,8 +773,21 @@ const Select = forwardRef( data-test={ariaLabel || name} autoClearSearchValue={autoClearSearchValue} popupRender={popupRender} - filterOption={handleFilterOption} - filterSort={sortComparatorWithSearch} + // Forward-compat: TS 6.0 infers stricter antd option types; local + // helpers typed against AntdLabeledValue are behaviorally compatible + // with the broader BaseOptionType/DefaultOptionType antd expects. + filterOption={ + handleFilterOption as unknown as ( + search: string, + option?: BaseOptionType | DefaultOptionType, + ) => boolean + } + filterSort={ + sortComparatorWithSearch as unknown as ( + a: BaseOptionType | DefaultOptionType, + b: BaseOptionType | DefaultOptionType, + ) => number + } getPopupContainer={ getPopupContainer || (triggerNode => triggerNode.parentNode) } @@ -763,13 +798,26 @@ const Select = forwardRef( mode={mappedMode} notFoundContent={isLoading ? t('Loading...') : notFoundContent} onBlur={handleOnBlur} - onDeselect={handleOnDeselect} + // Forward-compat: TS 6.0 narrows the Select value type handed to + // SelectHandler; our local handlers already accept the broader union. + onDeselect={ + handleOnDeselect as unknown as ( + value: unknown, + option: BaseOptionType | DefaultOptionType, + ) => void + } onOpenChange={handleOnDropdownVisibleChange} - // @ts-expect-error + // @ts-expect-error antd Select does not declare onPaste on its prop + // surface, but the underlying input accepts it and we rely on that. onPaste={onPaste} onPopupScroll={undefined} onSearch={shouldShowSearch ? handleOnSearch : undefined} - onSelect={handleOnSelect} + onSelect={ + handleOnSelect as unknown as ( + value: unknown, + option: BaseOptionType | DefaultOptionType, + ) => void + } onClear={handleClear} placeholder={placeholder} tokenSeparators={tokenSeparators} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx b/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx index b9bc81715a0..f5c6967b0e0 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/VirtualTable.tsx @@ -84,8 +84,8 @@ const VirtualTable = ( allowHTML = false, } = props; const [tableWidth, setTableWidth] = useState(0); - const onResize = useCallback((width: number) => { - setTableWidth(width); + const onResize = useCallback((width?: number) => { + setTableWidth(width ?? 0); }, []); const { ref } = useResizeDetector({ onResize }); const theme = useTheme(); diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.ts b/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.ts index 021ef81ff30..82dda4000aa 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/utils/InteractiveTableUtils.ts @@ -29,7 +29,7 @@ interface IInteractiveColumn extends HTMLElement { export default class InteractiveTableUtils { tableRef: HTMLTableElement | null; - columnRef: IInteractiveColumn | null; + columnRef: IInteractiveColumn | null = null; setDerivedColumns: Function; diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx index bc6ec88ab65..b74b2008717 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx @@ -27,7 +27,12 @@ import { } from 'react-table'; import { styled } from '@apache-superset/core/theme'; import { Table, TableSize } from '@superset-ui/core/components/Table'; -import { TableRowSelection, SorterResult } from 'antd/es/table/interface'; +import { + ColumnsType, + TableRowSelection, + SorterResult, +} from 'antd/es/table/interface'; +import type { TableProps } from 'antd/es/table'; import { mapColumns, mapRows } from './utils'; interface TableCollectionProps { @@ -290,7 +295,10 @@ function TableCollection({ surface the Table expects here. + columns={mappedColumns as unknown as ColumnsType} data={mappedRows} size={size} data-test="listview-table" @@ -303,7 +311,9 @@ function TableCollection({ sortDirections={['ascend', 'descend', 'ascend']} isPaginationSticky={isPaginationSticky} showRowCount={showRowCount} - rowClassName={getRowClassName} + rowClassName={ + getRowClassName as unknown as TableProps['rowClassName'] + } components={{ header: { cell: (props: HTMLAttributes) => ( @@ -319,7 +329,7 @@ function TableCollection({ ), }, }} - onChange={handleTableChange} + onChange={handleTableChange as unknown as TableProps['onChange']} /> ); } diff --git a/superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx index a790963e333..6e359c39458 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TimezoneSelector/index.tsx @@ -26,6 +26,7 @@ import { getOffsetKey, DEFAULT_TIMEZONE, } from './TimezoneOptionsCache'; +import type { LabeledValue } from 'antd/es/select'; import type { TimezoneOption } from './types'; // Import dayjs plugin types for TypeScript support @@ -156,7 +157,16 @@ export default function TimezoneSelector({ onOpenChange={handleOpenChange} value={selectValue} options={timezoneOptions || []} - sortComparator={sortComparator} + // Forward-compat: TS 6.0 resolves sortComparator against antd's + // LabeledValue; our comparator only reads properties that always exist + // on TimezoneOption, so the broader shape is safe at runtime. + sortComparator={ + sortComparator as unknown as ( + a: LabeledValue, + b: LabeledValue, + search?: string, + ) => number + } loading={isLoadingOptions} placeholder={isLoadingOptions ? t('Loading timezones...') : placeholder} {...{ placement: 'topLeft', ...rest }} diff --git a/superset-frontend/packages/superset-ui-core/src/connection/types.ts b/superset-frontend/packages/superset-ui-core/src/connection/types.ts index 47db5d1bc03..de8b2439c32 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/types.ts @@ -26,10 +26,18 @@ export type FetchRetryOptions = { retries?: number; retryDelay?: | number - | ((attempt: number, error: Error, response: Response) => number); + | (( + attempt: number, + error: Error | null, + response: Response | null, + ) => number); retryOn?: | number[] - | ((attempt: number, error: Error, response: Response) => boolean); + | (( + attempt: number, + error: Error | null, + response: Response | null, + ) => boolean); }; export type Headers = { [k: string]: string }; export type Host = string; diff --git a/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts b/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts index 61c2a2f8ad3..759a9f47111 100644 --- a/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts +++ b/superset-frontend/packages/superset-ui-core/src/time-comparison/fetchTimeRange.ts @@ -103,10 +103,16 @@ export const fetchTimeRange = async ( ), ), }; - } catch (response) { + } catch (caught) { + // Forward-compat: TS 6.0 types caught values as `unknown`; cast to the + // shape getClientErrorObject accepts and narrow for statusText access. + const response = caught as Parameters[0]; const clientError = await getClientErrorObject(response); return { - error: clientError.message || clientError.error || response.statusText, + error: + clientError.message || + clientError.error || + (response as { statusText?: string }).statusText, }; } }; diff --git a/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts b/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts index e92005986aa..7ac417e24df 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/lruCache.ts @@ -55,7 +55,12 @@ class LRUCache { throw new TypeError('The LRUCache key must be string.'); } if (this.cache.size >= this.capacity) { - this.cache.delete(this.cache.keys().next().value); + // Forward-compat: TS 6.0 types IteratorResult.value as `string | undefined` + // when not explicitly checked; guard before passing to Map#delete. + const oldestKey = this.cache.keys().next().value; + if (oldestKey !== undefined) { + this.cache.delete(oldestKey); + } } this.cache.set(key, value); } diff --git a/superset-frontend/packages/superset-ui-core/types/assets.d.ts b/superset-frontend/packages/superset-ui-core/types/assets.d.ts index b95944d3dba..b75c58d40f5 100644 --- a/superset-frontend/packages/superset-ui-core/types/assets.d.ts +++ b/superset-frontend/packages/superset-ui-core/types/assets.d.ts @@ -21,3 +21,4 @@ declare module '*.svg'; declare module '*.png'; declare module '*.jpg'; declare module '*.jpeg'; +declare module '*.css';