Commit 56e4e8c23a6 for woocommerce

commit 56e4e8c23a60a5872b6bbba3e0d9c5d71d3da46d
Author: Peter Petrov <peter.petrov89@gmail.com>
Date:   Fri Sep 4 10:23:17 2026 +0300

    Migrate @woocommerce/components AdvancedFilters to TypeScript (#68330)

    * Migrate @woocommerce/components AdvancedFilters to TypeScript

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * Export AdvancedFilters types from @woocommerce/components

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    * Type ReportHeader advancedFilters prop with AdvancedFilterConfig

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

diff --git a/packages/js/components/changelog/dev-migrate-advanced-filters-to-ts b/packages/js/components/changelog/dev-migrate-advanced-filters-to-ts
new file mode 100644
index 00000000000..825ab1ba26a
--- /dev/null
+++ b/packages/js/components/changelog/dev-migrate-advanced-filters-to-ts
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Migrate AdvancedFilters component to TS
diff --git a/packages/js/components/src/advanced-filters/attribute-filter.js b/packages/js/components/src/advanced-filters/attribute-filter.tsx
similarity index 82%
rename from packages/js/components/src/advanced-filters/attribute-filter.js
rename to packages/js/components/src/advanced-filters/attribute-filter.tsx
index 2883ae40df1..9781470aee8 100644
--- a/packages/js/components/src/advanced-filters/attribute-filter.js
+++ b/packages/js/components/src/advanced-filters/attribute-filter.tsx
@@ -1,7 +1,6 @@
 /**
  * External dependencies
  */
-import PropTypes from 'prop-types';
 import { SelectControl as Select, Spinner } from '@wordpress/components';
 import clsx from 'clsx';
 import {
@@ -22,6 +21,24 @@ import {
 	backwardsCompatibleCreateInterpolateElement as createInterpolateElement,
 	textContent,
 } from './utils';
+import type {
+	ActiveFilter,
+	FilterComponentProps,
+	FilterConfig,
+	FilterRule,
+} from './types';
+
+export type AttributeFilterProps = FilterComponentProps;
+
+type AttributeOption = {
+	key: string;
+	label: string;
+};
+
+type AttributeResponse = {
+	id: number;
+	name: string;
+};

 const getScreenReaderText = ( {
 	attributeTerms,
@@ -29,6 +46,12 @@ const getScreenReaderText = ( {
 	filter,
 	selectedAttribute,
 	selectedAttributeTerm,
+}: {
+	attributeTerms: AttributeOption[] | false;
+	config: FilterConfig;
+	filter: ActiveFilter;
+	selectedAttribute: AttributeOption[];
+	selectedAttributeTerm: string;
 } ) => {
 	if (
 		! attributeTerms ||
@@ -40,7 +63,7 @@ const getScreenReaderText = ( {
 		return '';
 	}

-	const rule = Array.isArray( config.rules )
+	const rule: Partial< FilterRule > = Array.isArray( config.rules )
 		? config.rules.find(
 				( configRule ) => configRule.value === filter.rule
 		  ) || {}
@@ -57,7 +80,6 @@ const getScreenReaderText = ( {
 	}

 	const filterStr = createInterpolateElement(
-		/* eslint-disable-next-line max-len */
 		/* translators: Sentence fragment describing a product attribute match. Example: "Color Is Not Blue" - attribute = Color, equals = Is Not, value = Blue */
 		__( '<attribute/> <equals/> <value/>', 'woocommerce' ),
 		{
@@ -76,12 +98,14 @@ const getScreenReaderText = ( {
 	);
 };

-const AttributeFilter = ( props ) => {
+const AttributeFilter = ( props: AttributeFilterProps ) => {
 	const { className, config, filter, isEnglish, onFilterChange } = props;
 	const { rule, value } = filter;
 	const { labels, rules } = config;

-	const [ selectedAttribute, setSelectedAttribute ] = useState( [] );
+	const [ selectedAttribute, setSelectedAttribute ] = useState<
+		AttributeOption[]
+	>( [] );

 	// Set selected attribute from filter value (in query string).
 	useEffect( () => {
@@ -90,7 +114,7 @@ const AttributeFilter = ( props ) => {
 			Array.isArray( value ) &&
 			value[ 0 ]
 		) {
-			apiFetch( {
+			void apiFetch< AttributeResponse >( {
 				path: `/wc-analytics/products/attributes/${ value[ 0 ] }`,
 			} )
 				.then( ( { id, name } ) => [
@@ -103,7 +127,9 @@ const AttributeFilter = ( props ) => {
 		}
 	}, [ value, selectedAttribute ] );

-	const [ attributeTerms, setAttributeTerms ] = useState( [] );
+	const [ attributeTerms, setAttributeTerms ] = useState<
+		AttributeOption[] | false
+	>( [] );

 	// Fetch all product attributes on mount.
 	useEffect( () => {
@@ -111,7 +137,7 @@ const AttributeFilter = ( props ) => {
 			return;
 		}
 		setAttributeTerms( false );
-		apiFetch( {
+		void apiFetch< AttributeResponse[] >( {
 			path: `/wc-analytics/products/attributes/${ selectedAttribute[ 0 ].key }/terms?per_page=100`,
 		} )
 			.then( ( terms ) =>
@@ -124,7 +150,7 @@ const AttributeFilter = ( props ) => {
 	}, [ selectedAttribute ] );

 	const [ selectedAttributeTerm, setSelectedAttributeTerm ] = useState(
-		Array.isArray( value ) ? value[ 1 ] || '' : ''
+		Array.isArray( value ) ? String( value[ 1 ] || '' ) : ''
 	);

 	const screenReaderText = getScreenReaderText( {
@@ -135,11 +161,10 @@ const AttributeFilter = ( props ) => {
 		selectedAttributeTerm,
 	} );

-	/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
 	return (
 		<fieldset
 			className="woocommerce-filters-advanced__line-item"
-			tabIndex="0"
+			tabIndex={ 0 }
 		>
 			<legend className="screen-reader-text">{ labels.add || '' }</legend>
 			<div
@@ -179,7 +204,9 @@ const AttributeFilter = ( props ) => {
 							selectedAttribute.length ? (
 								<Search
 									className="woocommerce-filters-advanced__input woocommerce-search"
-									onChange={ ( [ attr ] ) => {
+									onChange={ ( [
+										attr,
+									]: AttributeOption[] ) => {
 										setSelectedAttribute(
 											attr ? [ attr ] : []
 										);
@@ -208,13 +235,12 @@ const AttributeFilter = ( props ) => {
 								<Spinner />
 							) }
 							{ selectedAttribute.length > 0 &&
-								( attributeTerms.length ? (
+								( attributeTerms && attributeTerms.length ? (
 									<Fragment>
 										<span className="woocommerce-filters-advanced__attribute-field-separator">
 											=
 										</span>
 										<SelectControl
-											__next40pxDefaultSize
 											className="woocommerce-filters-advanced__input woocommerce-search"
 											placeholder={ __(
 												'Attribute value',
@@ -259,36 +285,6 @@ const AttributeFilter = ( props ) => {
 			) }
 		</fieldset>
 	);
-	/*eslint-enable jsx-a11y/no-noninteractive-tabindex*/
-};
-
-AttributeFilter.propTypes = {
-	/**
-	 * The configuration object for the single filter to be rendered.
-	 */
-	config: PropTypes.shape( {
-		labels: PropTypes.shape( {
-			rule: PropTypes.string,
-			title: PropTypes.string,
-			filter: PropTypes.string,
-		} ),
-		rules: PropTypes.arrayOf( PropTypes.object ),
-		input: PropTypes.object,
-	} ).isRequired,
-	/**
-	 * The activeFilter handed down by AdvancedFilters.
-	 */
-	filter: PropTypes.shape( {
-		key: PropTypes.string,
-		rule: PropTypes.string,
-		value: PropTypes.arrayOf(
-			PropTypes.oneOfType( [ PropTypes.string, PropTypes.number ] )
-		),
-	} ).isRequired,
-	/**
-	 * Function to be called on update.
-	 */
-	onFilterChange: PropTypes.func.isRequired,
 };

 export default AttributeFilter;
diff --git a/packages/js/components/src/advanced-filters/date-filter.js b/packages/js/components/src/advanced-filters/date-filter.tsx
similarity index 76%
rename from packages/js/components/src/advanced-filters/date-filter.js
rename to packages/js/components/src/advanced-filters/date-filter.tsx
index 460f5c83d77..719dbb0e8d9 100644
--- a/packages/js/components/src/advanced-filters/date-filter.js
+++ b/packages/js/components/src/advanced-filters/date-filter.tsx
@@ -8,6 +8,8 @@ import clsx from 'clsx';
 import { __, _x } from '@wordpress/i18n';
 import { isoDateFormat, toMoment } from '@woocommerce/date';
 import moment from 'moment';
+import type { Moment } from 'moment';
+import type { ReactNode } from 'react';

 /**
  * Internal dependencies
@@ -17,13 +19,35 @@ import {
 	backwardsCompatibleCreateInterpolateElement as createInterpolateElement,
 	textContent,
 } from './utils';
+import type { FilterComponentProps, FilterConfig, FilterRule } from './types';

 const dateStringFormat = __( 'MMM D, YYYY', 'woocommerce' );
 const dateFormat = __( 'MM/DD/YYYY', 'woocommerce' );

-class DateFilter extends Component {
-	constructor( { filter } ) {
-		super( ...arguments );
+export type DateFilterProps = FilterComponentProps;
+
+type DateUpdate = {
+	date: Moment | null;
+	text: string;
+	error: string | null;
+};
+
+type RangeInput = 'after' | 'before';
+
+type DateFilterState = {
+	before: Moment | null;
+	beforeText: string;
+	beforeError: string | null;
+	after: Moment | null;
+	afterText: string;
+	afterError: string | null;
+	rule?: string;
+};
+
+class DateFilter extends Component< DateFilterProps, DateFilterState > {
+	constructor( props: DateFilterProps ) {
+		super( props );
+		const { filter } = props;

 		const [ isoAfter, isoBefore ] = Array.isArray( filter.value )
 			? filter.value
@@ -54,8 +78,12 @@ class DateFilter extends Component {
 		);
 	}

-	getScreenReaderText( filterRule, config ) {
-		const rule = find( config.rules, { value: filterRule } ) || {};
+	getScreenReaderText(
+		filterRule: string | undefined,
+		config: FilterConfig
+	) {
+		const rule: Partial< FilterRule > =
+			find( config.rules, { value: filterRule } ) || {};

 		const { before, after } = this.state;

@@ -64,9 +92,9 @@ class DateFilter extends Component {
 			return '';
 		}

-		let filterStr = before.format( dateStringFormat );
+		let filterStr: ReactNode = before.format( dateStringFormat );

-		if ( rule.value === 'between' ) {
+		if ( rule.value === 'between' && after ) {
 			filterStr = createInterpolateElement( this.getBetweenString(), {
 				after: (
 					<Fragment>{ after.format( dateStringFormat ) }</Fragment>
@@ -87,7 +115,7 @@ class DateFilter extends Component {
 		);
 	}

-	onSingleDateChange( { date, text, error } ) {
+	onSingleDateChange( { date, text, error }: DateUpdate ) {
 		const { onFilterChange } = this.props;
 		this.setState( { before: date, beforeText: text, beforeError: error } );

@@ -99,14 +127,22 @@ class DateFilter extends Component {
 		}
 	}

-	onRangeDateChange( input, { date, text, error } ) {
+	onRangeDateChange( input: RangeInput, { date, text, error }: DateUpdate ) {
 		const { onFilterChange } = this.props;

-		this.setState( {
-			[ input ]: date,
-			[ input + 'Text' ]: text,
-			[ input + 'Error' ]: error,
-		} );
+		if ( input === 'after' ) {
+			this.setState( {
+				after: date,
+				afterText: text,
+				afterError: error,
+			} );
+		} else {
+			this.setState( {
+				before: date,
+				beforeText: text,
+				beforeError: error,
+			} );
+		}

 		if ( date ) {
 			const { before, after } = this.state;
@@ -132,31 +168,26 @@ class DateFilter extends Component {
 		}
 	}

-	onRuleChange( newRule ) {
+	onRuleChange( newRule: string ) {
 		const { onFilterChange } = this.props;
 		const { rule } = this.state;

-		let newDateState = null;
-		let shouldResetValue = false;
+		const shouldResetValue = [ rule, newRule ].includes( 'between' );

-		if ( [ rule, newRule ].includes( 'between' ) ) {
-			newDateState = {
+		if ( shouldResetValue ) {
+			this.setState( {
+				rule: newRule,
 				before: null,
 				beforeText: '',
 				beforeError: null,
 				after: null,
 				afterText: '',
 				afterError: null,
-			};
-
-			shouldResetValue = true;
+			} );
+		} else {
+			this.setState( { rule: newRule } );
 		}

-		this.setState( {
-			rule: newRule,
-			...newDateState,
-		} );
-
 		onFilterChange( {
 			property: 'rule',
 			value: newRule,
@@ -164,11 +195,16 @@ class DateFilter extends Component {
 		} );
 	}

-	isFutureDate( dateString ) {
-		return moment().isBefore( moment( dateString ), 'day' );
+	isFutureDate( date: Date ) {
+		return moment().isBefore( moment( date ), 'day' );
 	}

-	getFormControl( { date, error, onUpdate, text } ) {
+	getFormControl( {
+		date,
+		error,
+		onUpdate,
+		text,
+	}: DateUpdate & { onUpdate: ( update: DateUpdate ) => void } ) {
 		return (
 			<DatePicker
 				date={ date }
@@ -256,11 +292,11 @@ class DateFilter extends Component {
 				</div>
 			),
 		} );
-		/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
+
 		return (
 			<fieldset
 				className="woocommerce-filters-advanced__line-item"
-				tabIndex="0"
+				tabIndex={ 0 }
 			>
 				<legend className="screen-reader-text">
 					{ labels.add || '' }
@@ -282,7 +318,6 @@ class DateFilter extends Component {
 				) }
 			</fieldset>
 		);
-		/*eslint-enable jsx-a11y/no-noninteractive-tabindex*/
 	}
 }

diff --git a/packages/js/components/src/advanced-filters/index.js b/packages/js/components/src/advanced-filters/index.tsx
similarity index 79%
rename from packages/js/components/src/advanced-filters/index.js
rename to packages/js/components/src/advanced-filters/index.tsx
index 0ab8078c919..ac4a81c869a 100644
--- a/packages/js/components/src/advanced-filters/index.js
+++ b/packages/js/components/src/advanced-filters/index.tsx
@@ -13,7 +13,6 @@ import {
 } from '@wordpress/components';
 import { createElement, Component, createRef } from '@wordpress/element';
 import { partial, isEqual } from 'lodash';
-import PropTypes from 'prop-types';
 import AddOutlineIcon from 'gridicons/dist/add-outline';
 import {
 	getActiveFiltersFromQuery,
@@ -30,6 +29,70 @@ import Link from '../link';
 import AdvancedFilterItem from './item';
 import { Text } from '../experimental';
 import { backwardsCompatibleCreateInterpolateElement as createInterpolateElement } from './utils';
+import type {
+	ActiveFilter,
+	AdvancedFilterAction,
+	AdvancedFilterConfig,
+	Currency,
+	FilterChange,
+	FilterConfig,
+	Query,
+} from './types';
+
+export type {
+	ActiveFilter,
+	ActiveFilterValue,
+	AdvancedFilterAction,
+	AdvancedFilterConfig,
+	FilterChange,
+	FilterConfig,
+	FilterInput,
+	FilterLabels,
+	FilterOption,
+	FilterRule,
+	Query,
+} from './types';
+
+type AdvancedFiltersDefaultProps = {
+	/**
+	 * The query string represented in object form.
+	 */
+	query: Query;
+	/**
+	 * Function to be called after an advanced filter action has been taken.
+	 */
+	onAdvancedFilterAction: (
+		action: AdvancedFilterAction,
+		data?: ActiveFilter | Record< string, unknown >
+	) => void;
+	/**
+	 * The locale for the site.
+	 */
+	siteLocale: string;
+};
+
+export type AdvancedFiltersProps = {
+	/**
+	 * The configuration object required to render filters.
+	 */
+	config: AdvancedFilterConfig;
+	/**
+	 * Name of this filter, used in translations.
+	 */
+	path: string;
+	/**
+	 * The currency formatting instance for the site.
+	 */
+	currency: Currency;
+} & Partial< AdvancedFiltersDefaultProps >;
+
+// Inside the class the defaulted props are always present.
+type Props = AdvancedFiltersProps & AdvancedFiltersDefaultProps;
+
+type AdvancedFiltersState = {
+	match: string;
+	activeFilters: ActiveFilter[];
+};

 const matches = [
 	{ value: 'all', label: __( 'All', 'woocommerce' ) },
@@ -39,12 +102,22 @@ const matches = [
 /**
  * Displays a configurable set of filters which can modify query parameters.
  */
-class AdvancedFilters extends Component {
-	constructor( { query, config } ) {
-		super( ...arguments );
-		this.instanceCounts = {};
+class AdvancedFilters extends Component< Props, AdvancedFiltersState > {
+	static defaultProps: AdvancedFiltersDefaultProps = {
+		query: {},
+		onAdvancedFilterAction: () => {},
+		siteLocale: 'en_US',
+	};
+
+	instanceCounts: Record< string, number > = {};
+
+	filterListRef = createRef< HTMLUListElement >();

-		const filtersFromQuery = getActiveFiltersFromQuery(
+	constructor( props: Props ) {
+		super( props );
+		const { query, config } = props;
+
+		const filtersFromQuery: ActiveFilter[] = getActiveFiltersFromQuery(
 			query,
 			config.filters
 		);
@@ -58,12 +131,13 @@ class AdvancedFilters extends Component {
 		} );

 		this.state = {
-			match: query.match || 'all',
+			match:
+				typeof query.match === 'string' && query.match
+					? query.match
+					: 'all',
 			activeFilters,
 		};

-		this.filterListRef = createRef();
-
 		this.onMatchChange = this.onMatchChange.bind( this );
 		this.onFilterChange = this.onFilterChange.bind( this );
 		this.getAvailableFilters = this.getAvailableFilters.bind( this );
@@ -74,12 +148,12 @@ class AdvancedFilters extends Component {
 		this.onFilter = this.onFilter.bind( this );
 	}

-	componentDidUpdate( prevProps ) {
+	componentDidUpdate( prevProps: Props ) {
 		const { config, query } = this.props;
 		const { query: prevQuery } = prevProps;

 		if ( ! isEqual( prevQuery, query ) ) {
-			const filtersFromQuery = getActiveFiltersFromQuery(
+			const filtersFromQuery: ActiveFilter[] = getActiveFiltersFromQuery(
 				query,
 				config.filters
 			);
@@ -95,13 +169,11 @@ class AdvancedFilters extends Component {
 				return filter;
 			} );

-			/* eslint-disable react/no-did-update-set-state */
 			this.setState( { activeFilters } );
-			/* eslint-enable react/no-did-update-set-state */
 		}
 	}

-	getInstanceNumber( key ) {
+	getInstanceNumber( key: string ) {
 		if ( ! this.instanceCounts.hasOwnProperty( key ) ) {
 			this.instanceCounts[ key ] = 1;
 		}
@@ -109,7 +181,7 @@ class AdvancedFilters extends Component {
 		return this.instanceCounts[ key ]++;
 	}

-	onMatchChange( match ) {
+	onMatchChange( match: string ) {
 		const { onAdvancedFilterAction } = this.props;

 		this.setState( { match } );
@@ -117,7 +189,10 @@ class AdvancedFilters extends Component {
 		onAdvancedFilterAction( 'match', { match } );
 	}

-	onFilterChange( index, { property, value, shouldResetValue = false } ) {
+	onFilterChange(
+		index: number,
+		{ property, value, shouldResetValue = false }: FilterChange
+	) {
 		const newActiveFilters = [ ...this.state.activeFilters ];
 		newActiveFilters[ index ] = {
 			...newActiveFilters[ index ],
@@ -128,7 +203,7 @@ class AdvancedFilters extends Component {
 		this.setState( { activeFilters: newActiveFilters } );
 	}

-	removeFilter( index ) {
+	removeFilter( index: number ) {
 		const { onAdvancedFilterAction } = this.props;
 		const activeFilters = [ ...this.state.activeFilters ];
 		onAdvancedFilterAction( 'remove', activeFilters[ index ] );
@@ -186,10 +261,10 @@ class AdvancedFilters extends Component {
 		return availableFilters;
 	}

-	addFilter( key, onClose ) {
+	addFilter( key: string, onClose: () => void ) {
 		const { onAdvancedFilterAction, config } = this.props;
-		const filterConfig = config.filters[ key ];
-		const newFilter = { key };
+		const filterConfig: FilterConfig = config.filters[ key ];
+		const newFilter: ActiveFilter = { key };
 		if (
 			Array.isArray( filterConfig.rules ) &&
 			filterConfig.rules.length
@@ -217,10 +292,11 @@ class AdvancedFilters extends Component {
 		onClose();
 		// after render, focus the newly added filter's first focusable element
 		setTimeout( () => {
-			const addedFilter = this.filterListRef.current.querySelector(
-				'li:last-of-type fieldset'
-			);
-			addedFilter.focus();
+			const addedFilter =
+				this.filterListRef.current?.querySelector< HTMLElement >(
+					'li:last-of-type fieldset'
+				);
+			addedFilter?.focus();
 		} );
 	}

@@ -233,7 +309,7 @@ class AdvancedFilters extends Component {
 		} );
 	}

-	getUpdateHref( activeFilters, matchValue ) {
+	getUpdateHref( activeFilters: ActiveFilter[], matchValue?: string ) {
 		const { path, query, config } = this.props;
 		const updatedQuery = getQueryFromActiveFilters(
 			activeFilters,
@@ -259,7 +335,7 @@ class AdvancedFilters extends Component {
 		onAdvancedFilterAction( 'filter', { ...updatedQuery, match } );
 	}

-	orderFilters( a, b ) {
+	orderFilters( a: ActiveFilter, b: ActiveFilter ) {
 		const qs = window.location.search;
 		const aPos = qs.indexOf( a.key );
 		const bPos = qs.indexOf( b.key );
@@ -282,6 +358,8 @@ class AdvancedFilters extends Component {
 		const isEnglish = this.isEnglish();
 		return (
 			<Card className="woocommerce-filters-advanced" size="small">
+				{ /* CardHeader forwards unknown props to Flex, so `justify` works but isn't typed. */ }
+				{ /* @ts-expect-error: justify is not a declared CardHeader prop. */ }
 				<CardHeader justify="flex-start">
 					<Text
 						variant="subtitle.small"
@@ -295,6 +373,8 @@ class AdvancedFilters extends Component {
 					</Text>
 				</CardHeader>
 				{ !! activeFilters.length && (
+					// An unknown size maps to no padding class, which is what the list relies on.
+					// @ts-expect-error: size must be one of small, medium, large, xSmall, extraSmall.
 					<CardBody size="none">
 						<ul
 							className="woocommerce-filters-advanced__list"
@@ -364,6 +444,8 @@ class AdvancedFilters extends Component {
 						</div>
 					</CardBody>
 				) }
+				{ /* CardFooter forwards unknown props to Flex, so `align` works but isn't typed. */ }
+				{ /* @ts-expect-error: align is not a declared CardFooter prop. */ }
 				<CardFooter align="center">
 					<div className="woocommerce-filters-advanced__controls">
 						{ updateDisabled && (
@@ -397,52 +479,4 @@ class AdvancedFilters extends Component {
 	}
 }

-AdvancedFilters.propTypes = {
-	/**
-	 * The configuration object required to render filters.
-	 */
-	config: PropTypes.shape( {
-		title: PropTypes.string,
-		filters: PropTypes.objectOf(
-			PropTypes.shape( {
-				labels: PropTypes.shape( {
-					add: PropTypes.string,
-					remove: PropTypes.string,
-					rule: PropTypes.string,
-					title: PropTypes.string,
-					filter: PropTypes.string,
-				} ),
-				rules: PropTypes.arrayOf( PropTypes.object ),
-				input: PropTypes.object,
-			} )
-		),
-	} ).isRequired,
-	/**
-	 * Name of this filter, used in translations.
-	 */
-	path: PropTypes.string.isRequired,
-	/**
-	 * The query string represented in object form.
-	 */
-	query: PropTypes.object,
-	/**
-	 * Function to be called after an advanced filter action has been taken.
-	 */
-	onAdvancedFilterAction: PropTypes.func,
-	/**
-	 * The locale for the site.
-	 */
-	siteLocale: PropTypes.string,
-	/**
-	 * The currency formatting instance for the site.
-	 */
-	currency: PropTypes.object.isRequired,
-};
-
-AdvancedFilters.defaultProps = {
-	query: {},
-	onAdvancedFilterAction: () => {},
-	siteLocale: 'en_US',
-};
-
 export default AdvancedFilters;
diff --git a/packages/js/components/src/advanced-filters/item.js b/packages/js/components/src/advanced-filters/item.tsx
similarity index 64%
rename from packages/js/components/src/advanced-filters/item.js
rename to packages/js/components/src/advanced-filters/item.tsx
index 3b84ec857b4..41345e26dfa 100644
--- a/packages/js/components/src/advanced-filters/item.js
+++ b/packages/js/components/src/advanced-filters/item.tsx
@@ -14,8 +14,40 @@ import SearchFilter from './search-filter';
 import NumberFilter from './number-filter';
 import DateFilter from './date-filter';
 import AttributeFilter from './attribute-filter';
+import type {
+	ActiveFilter,
+	AdvancedFilterConfig,
+	Currency,
+	FilterConfig,
+	OnFilterChange,
+	Query,
+} from './types';

-const AdvancedFilterItem = ( props ) => {
+export type AdvancedFilterItemProps = {
+	config: AdvancedFilterConfig;
+	currency: Currency;
+	filter: ActiveFilter;
+	isEnglish: boolean;
+	onFilterChange: OnFilterChange;
+	query: Query;
+	removeFilter: () => void;
+};
+
+const componentMap = {
+	Currency: NumberFilter,
+	Date: DateFilter,
+	Number: NumberFilter,
+	ProductAttribute: AttributeFilter,
+	Search: SearchFilter,
+	SelectControl: SelectFilter,
+};
+
+const isKnownComponent = (
+	component: string
+): component is keyof typeof componentMap =>
+	componentMap.hasOwnProperty( component );
+
+const AdvancedFilterItem = ( props: AdvancedFilterItemProps ) => {
 	const {
 		config,
 		currency,
@@ -26,20 +58,11 @@ const AdvancedFilterItem = ( props ) => {
 		removeFilter,
 	} = props;
 	const { key } = filterValue;
-	let filterConfig = config.filters[ key ];
+	let filterConfig: FilterConfig = config.filters[ key ];
 	const { input, labels } = filterConfig;

-	const componentMap = {
-		Currency: NumberFilter,
-		Date: DateFilter,
-		Number: NumberFilter,
-		ProductAttribute: AttributeFilter,
-		Search: SearchFilter,
-		SelectControl: SelectFilter,
-	};
-
-	if ( ! componentMap.hasOwnProperty( input.component ) ) {
-		return;
+	if ( ! isKnownComponent( input.component ) ) {
+		return null;
 	}

 	if ( input.component === 'Currency' ) {
diff --git a/packages/js/components/src/advanced-filters/number-filter.js b/packages/js/components/src/advanced-filters/number-filter.tsx
similarity index 74%
rename from packages/js/components/src/advanced-filters/number-filter.js
rename to packages/js/components/src/advanced-filters/number-filter.tsx
index d3b25c0f78b..39218e53705 100644
--- a/packages/js/components/src/advanced-filters/number-filter.js
+++ b/packages/js/components/src/advanced-filters/number-filter.tsx
@@ -7,17 +7,53 @@ import { get, find, isArray } from 'lodash';
 import clsx from 'clsx';
 import { sprintf, __, _x } from '@wordpress/i18n';
 import { CurrencyFactory } from '@woocommerce/currency';
+import type { CurrencyConfig } from '@woocommerce/currency';
+import type { ComponentType, ReactNode } from 'react';

 /**
  * Internal dependencies
  */
-import TextControlWithAffixes from '../text-control-with-affixes';
+import TextControlWithAffixesBase from '../text-control-with-affixes';
 import {
 	backwardsCompatibleCreateInterpolateElement as createInterpolateElement,
 	textContent,
 } from './utils';
+import type {
+	ActiveFilter,
+	Currency,
+	FilterComponentProps,
+	FilterConfig,
+	FilterRule,
+} from './types';

-class NumberFilter extends Component {
+export type NumberFilterProps = FilterComponentProps & {
+	currency: Currency;
+};
+
+type TextControlWithAffixesProps = {
+	className?: string;
+	type?: string;
+	value: string | number;
+	onChange: ( value: string ) => void;
+	prefix?: ReactNode;
+	suffix?: ReactNode;
+	'aria-label'?: string;
+};
+
+// The component is wrapped in withInstanceId, which hides its props from TS.
+const TextControlWithAffixes =
+	TextControlWithAffixesBase as unknown as ComponentType< TextControlWithAffixesProps >;
+
+type FormControlArgs = {
+	type: string;
+	value: string | number | null | undefined;
+	label: string;
+	onChange: ( value: string ) => void;
+	currencySymbol: string;
+	symbolPosition: string;
+};
+
+class NumberFilter extends Component< NumberFilterProps > {
 	getBetweenString() {
 		return _x(
 			'<rangeStart/><span> and </span><rangeEnd/>',
@@ -26,9 +62,10 @@ class NumberFilter extends Component {
 		);
 	}

-	getScreenReaderText( filter, config ) {
+	getScreenReaderText( filter: ActiveFilter, config: FilterConfig ) {
 		const { currency } = this.props;
-		const rule = find( config.rules, { value: filter.rule } ) || {};
+		const rule: Partial< FilterRule > =
+			find( config.rules, { value: filter.rule } ) || {};
 		let [ rangeStart, rangeEnd ] = isArray( filter.value )
 			? filter.value
 			: [ filter.value ];
@@ -40,12 +77,16 @@ class NumberFilter extends Component {
 		const inputType = get( config, [ 'input', 'type' ], 'number' );

 		if ( inputType === 'currency' ) {
-			const { formatAmount } = CurrencyFactory( currency );
+			const { formatAmount } = CurrencyFactory(
+				currency as CurrencyConfig
+			);
 			rangeStart = formatAmount( rangeStart );
-			rangeEnd = formatAmount( rangeEnd );
+			if ( rangeEnd ) {
+				rangeEnd = formatAmount( rangeEnd );
+			}
 		}

-		let filterStr = rangeStart;
+		let filterStr: ReactNode = rangeStart;

 		if ( rule.value === 'between' ) {
 			filterStr = createInterpolateElement( this.getBetweenString(), {
@@ -71,7 +112,7 @@ class NumberFilter extends Component {
 		onChange,
 		currencySymbol,
 		symbolPosition,
-	} ) {
+	}: FormControlArgs ) {
 		if ( type === 'currency' ) {
 			return symbolPosition.indexOf( 'right' ) === 0 ? (
 				<TextControlWithAffixes
@@ -117,7 +158,7 @@ class NumberFilter extends Component {
 		const [ rangeStart, rangeEnd ] = isArray( filter.value )
 			? filter.value
 			: [ filter.value ];
-		if ( Boolean( rangeEnd ) ) {
+		if ( rangeEnd ) {
 			// If there's a value for rangeEnd, we've just changed from "between"
 			// to "less than" or "more than" and need to transition the value
 			onFilterChange( {
@@ -126,23 +167,20 @@ class NumberFilter extends Component {
 			} );
 		}

-		let labelFormat = '';
-
-		if ( filter.rule === 'lessthan' ) {
-			/* translators: Sentence fragment, "maximum amount" refers to a numeric value the field must be less than. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
-			labelFormat = _x(
-				'%(field)s maximum amount',
-				'maximum value input',
-				'woocommerce'
-			);
-		} else {
-			/* translators: Sentence fragment, "minimum amount" refers to a numeric value the field must be more than. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
-			labelFormat = _x(
-				'%(field)s minimum amount',
-				'minimum value input',
-				'woocommerce'
-			);
-		}
+		const labelFormat =
+			filter.rule === 'lessthan'
+				? /* translators: Sentence fragment, "maximum amount" refers to a numeric value the field must be less than. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
+				  _x(
+						'%(field)s maximum amount',
+						'maximum value input',
+						'woocommerce'
+				  )
+				: /* translators: Sentence fragment, "minimum amount" refers to a numeric value the field must be more than. Screenshot for context: https://cloudup.com/cmv5CLyMPNQ */
+				  _x(
+						'%(field)s minimum amount',
+						'minimum value input',
+						'woocommerce'
+				  );

 		return this.getFormControl( {
 			type: inputType,
@@ -165,14 +203,14 @@ class NumberFilter extends Component {
 			? filter.value
 			: [ filter.value ];

-		const rangeStartOnChange = ( newRangeStart ) => {
+		const rangeStartOnChange = ( newRangeStart: string ) => {
 			onFilterChange( {
 				property: 'value',
 				value: [ newRangeStart, rangeEnd ],
 			} );
 		};

-		const rangeEndOnChange = ( newRangeEnd ) => {
+		const rangeEndOnChange = ( newRangeEnd: string ) => {
 			onFilterChange( {
 				property: 'value',
 				value: [ rangeStart, newRangeEnd ],
@@ -248,11 +286,10 @@ class NumberFilter extends Component {

 		const screenReaderText = this.getScreenReaderText( filter, config );

-		/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
 		return (
 			<fieldset
 				className="woocommerce-filters-advanced__line-item"
-				tabIndex="0"
+				tabIndex={ 0 }
 			>
 				<legend className="screen-reader-text">
 					{ labels.add || '' }
@@ -274,7 +311,6 @@ class NumberFilter extends Component {
 				) }
 			</fieldset>
 		);
-		/*eslint-enable jsx-a11y/no-noninteractive-tabindex*/
 	}
 }

diff --git a/packages/js/components/src/advanced-filters/search-filter.js b/packages/js/components/src/advanced-filters/search-filter.tsx
similarity index 71%
rename from packages/js/components/src/advanced-filters/search-filter.js
rename to packages/js/components/src/advanced-filters/search-filter.tsx
index e9fc6da8e86..d333a7db60a 100644
--- a/packages/js/components/src/advanced-filters/search-filter.js
+++ b/packages/js/components/src/advanced-filters/search-filter.tsx
@@ -4,28 +4,53 @@
 import { createElement, Component, Fragment } from '@wordpress/element';
 import { SelectControl } from '@wordpress/components';
 import { find, isEqual } from 'lodash';
-import PropTypes from 'prop-types';
 import clsx from 'clsx';

 /**
  * Internal dependencies
  */
 import Search from '../search';
+import type { SearchProps, SearchType } from '../search';
 import {
 	backwardsCompatibleCreateInterpolateElement as createInterpolateElement,
 	textContent,
 } from './utils';
+import type {
+	ActiveFilter,
+	ActiveFilterValue,
+	FilterComponentProps,
+	FilterConfig,
+	FilterRule,
+	Query,
+	SearchLabel,
+} from './types';
+
+export type SearchFilterProps = FilterComponentProps;

-const normalizeFilterValue = ( value ) => {
+/**
+ * A selected search value. Keys come from API ids, so they may be numeric at runtime.
+ */
+export type SearchSelection = {
+	key: string | number;
+	label: string;
+	id?: string | number;
+};
+
+type SearchFilterState = {
+	selected: SearchSelection[];
+};
+
+const normalizeFilterValue = ( value: ActiveFilterValue | undefined ) => {
 	if ( Array.isArray( value ) ) {
 		return value.join( ',' );
 	}
 	return typeof value === 'string' ? value : '';
 };

-class SearchFilter extends Component {
-	constructor( { filter, query } ) {
-		super( ...arguments );
+class SearchFilter extends Component< SearchFilterProps, SearchFilterState > {
+	constructor( props: SearchFilterProps ) {
+		super( props );
+		const { filter, query } = props;
 		this.onSearchChange = this.onSearchChange.bind( this );
 		this.state = {
 			selected: [],
@@ -39,7 +64,7 @@ class SearchFilter extends Component {
 		}
 	}

-	componentDidUpdate( prevProps ) {
+	componentDidUpdate( prevProps: SearchFilterProps ) {
 		const { filter, query } = this.props;
 		const { filter: prevFilter } = prevProps;
 		const filterValue = normalizeFilterValue( filter.value );
@@ -61,9 +86,9 @@ class SearchFilter extends Component {
 		}
 	}

-	loadLabels( filterValue, query ) {
-		this.props.config.input
-			.getLabels( filterValue, query )
+	loadLabels( filterValue: string, query?: Query ) {
+		void this.props.config.input
+			.getLabels?.( filterValue, query )
 			.then( ( selected ) => {
 				if (
 					filterValue ===
@@ -74,11 +99,15 @@ class SearchFilter extends Component {
 			} );
 	}

-	updateLabels( selected ) {
-		const normalizedSelected = selected.map( ( item ) => ( {
-			...item,
-			key: item.key ?? item.id,
-		} ) );
+	updateLabels( selected: SearchLabel[] ) {
+		const normalizedSelected = selected
+			.map( ( item ) => ( {
+				...item,
+				key: item.key ?? item.id,
+			} ) )
+			.filter(
+				( item ): item is SearchSelection => item.key !== undefined
+			);
 		const prevIds = this.state.selected.map( ( item ) => item.key );
 		const ids = normalizedSelected.map( ( item ) => item.key );

@@ -87,7 +116,7 @@ class SearchFilter extends Component {
 		}
 	}

-	onSearchChange( values ) {
+	onSearchChange( values: SearchSelection[] ) {
 		this.setState( {
 			selected: values,
 		} );
@@ -96,14 +125,15 @@ class SearchFilter extends Component {
 		onFilterChange( { property: 'value', value: idList } );
 	}

-	getScreenReaderText( filter, config ) {
+	getScreenReaderText( filter: ActiveFilter, config: FilterConfig ) {
 		const { selected } = this.state;

 		if ( selected.length === 0 ) {
 			return '';
 		}

-		const rule = find( config.rules, { value: filter.rule } ) || {};
+		const rule: Partial< FilterRule > =
+			find( config.rules, { value: filter.rule } ) || {};
 		const filterStr = selected.map( ( item ) => item.label ).join( ', ' );

 		return textContent(
@@ -145,10 +175,11 @@ class SearchFilter extends Component {
 						'woocommerce-filters-advanced__input'
 					) }
 					onChange={ this.onSearchChange }
-					type={ input.type }
+					type={ input.type as SearchType }
 					autocompleter={ input.autocompleter }
 					placeholder={ labels.placeholder }
-					selected={ selected }
+					// Search types keys as strings, but ids resolved from the API are numeric.
+					selected={ selected as SearchProps[ 'selected' ] }
 					inlineTags
 					aria-label={ labels.filter }
 				/>
@@ -160,7 +191,7 @@ class SearchFilter extends Component {
 		return (
 			<fieldset
 				className="woocommerce-filters-advanced__line-item"
-				tabIndex="0"
+				tabIndex={ 0 }
 			>
 				<legend className="screen-reader-text">
 					{ labels.add || '' }
@@ -185,35 +216,4 @@ class SearchFilter extends Component {
 	}
 }

-SearchFilter.propTypes = {
-	/**
-	 * The configuration object for the single filter to be rendered.
-	 */
-	config: PropTypes.shape( {
-		labels: PropTypes.shape( {
-			placeholder: PropTypes.string,
-			rule: PropTypes.string,
-			title: PropTypes.string,
-		} ),
-		rules: PropTypes.arrayOf( PropTypes.object ),
-		input: PropTypes.object,
-	} ).isRequired,
-	/**
-	 * The activeFilter handed down by AdvancedFilters.
-	 */
-	filter: PropTypes.shape( {
-		key: PropTypes.string,
-		rule: PropTypes.string,
-		value: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array ] ),
-	} ).isRequired,
-	/**
-	 * Function to be called on update.
-	 */
-	onFilterChange: PropTypes.func.isRequired,
-	/**
-	 * The query string represented in object form.
-	 */
-	query: PropTypes.object,
-};
-
 export default SearchFilter;
diff --git a/packages/js/components/src/advanced-filters/select-filter.js b/packages/js/components/src/advanced-filters/select-filter.tsx
similarity index 71%
rename from packages/js/components/src/advanced-filters/select-filter.js
rename to packages/js/components/src/advanced-filters/select-filter.tsx
index f54c89723e1..053edf5089e 100644
--- a/packages/js/components/src/advanced-filters/select-filter.js
+++ b/packages/js/components/src/advanced-filters/select-filter.tsx
@@ -4,7 +4,6 @@
 import { createElement, Component, Fragment } from '@wordpress/element';
 import { SelectControl, Spinner } from '@wordpress/components';
 import { find } from 'lodash';
-import PropTypes from 'prop-types';
 import clsx from 'clsx';
 import { getDefaultOptionValue } from '@woocommerce/navigation';

@@ -15,10 +14,24 @@ import {
 	backwardsCompatibleCreateInterpolateElement as createInterpolateElement,
 	textContent,
 } from './utils';
+import type {
+	ActiveFilter,
+	FilterComponentProps,
+	FilterConfig,
+	FilterOption,
+	FilterRule,
+} from './types';

-class SelectFilter extends Component {
-	constructor( { filter, config, onFilterChange } ) {
-		super( ...arguments );
+export type SelectFilterProps = FilterComponentProps;
+
+type SelectFilterState = {
+	options?: FilterOption[];
+};
+
+class SelectFilter extends Component< SelectFilterProps, SelectFilterState > {
+	constructor( props: SelectFilterProps ) {
+		super( props );
+		const { filter, config, onFilterChange } = props;

 		const options = config.input.options;
 		this.state = { options };
@@ -26,7 +39,7 @@ class SelectFilter extends Component {
 		this.updateOptions = this.updateOptions.bind( this );

 		if ( ! options && config.input.getOptions ) {
-			config.input
+			void config.input
 				.getOptions()
 				.then( this.updateOptions )
 				.then( ( returnedOptions ) => {
@@ -41,19 +54,23 @@ class SelectFilter extends Component {
 		}
 	}

-	updateOptions( options ) {
+	updateOptions( options: FilterOption[] ) {
 		this.setState( { options } );
 		return options;
 	}

-	getScreenReaderText( filter, config ) {
+	getScreenReaderText( filter: ActiveFilter, config: FilterConfig ) {
 		if ( filter.value === '' ) {
 			return '';
 		}

-		const rule = find( config.rules, { value: filter.rule } ) || {};
-		const value =
-			find( config.input.options, { value: filter.value } ) || {};
+		const rule: Partial< FilterRule > =
+			find( config.rules, { value: filter.rule } ) || {};
+		const value: Partial< FilterOption > =
+			find(
+				config.input.options,
+				( option ) => option.value === filter.value
+			) || {};

 		return textContent(
 			createInterpolateElement( config.labels.title, {
@@ -98,7 +115,7 @@ class SelectFilter extends Component {
 						'woocommerce-filters-advanced__input'
 					) }
 					options={ options }
-					value={ value }
+					value={ String( value ?? '' ) }
 					onChange={ ( selectedValue ) =>
 						onFilterChange( {
 							property: 'value',
@@ -114,11 +131,10 @@ class SelectFilter extends Component {

 		const screenReaderText = this.getScreenReaderText( filter, config );

-		/*eslint-disable jsx-a11y/no-noninteractive-tabindex*/
 		return (
 			<fieldset
 				className="woocommerce-filters-advanced__line-item"
-				tabIndex="0"
+				tabIndex={ 0 }
 			>
 				<legend className="screen-reader-text">
 					{ labels.add || '' }
@@ -140,35 +156,7 @@ class SelectFilter extends Component {
 				) }
 			</fieldset>
 		);
-		/*eslint-enable jsx-a11y/no-noninteractive-tabindex*/
 	}
 }

-SelectFilter.propTypes = {
-	/**
-	 * The configuration object for the single filter to be rendered.
-	 */
-	config: PropTypes.shape( {
-		labels: PropTypes.shape( {
-			rule: PropTypes.string,
-			title: PropTypes.string,
-			filter: PropTypes.string,
-		} ),
-		rules: PropTypes.arrayOf( PropTypes.object ),
-		input: PropTypes.object,
-	} ).isRequired,
-	/**
-	 * The activeFilter handed down by AdvancedFilters.
-	 */
-	filter: PropTypes.shape( {
-		key: PropTypes.string,
-		rule: PropTypes.string,
-		value: PropTypes.string,
-	} ).isRequired,
-	/**
-	 * Function to be called on update.
-	 */
-	onFilterChange: PropTypes.func.isRequired,
-};
-
 export default SelectFilter;
diff --git a/packages/js/components/src/advanced-filters/stories/advanced-filters.story.js b/packages/js/components/src/advanced-filters/stories/advanced-filters.story.tsx
similarity index 90%
rename from packages/js/components/src/advanced-filters/stories/advanced-filters.story.js
rename to packages/js/components/src/advanced-filters/stories/advanced-filters.story.tsx
index d93f500b881..33b5028c1ca 100644
--- a/packages/js/components/src/advanced-filters/stories/advanced-filters.story.js
+++ b/packages/js/components/src/advanced-filters/stories/advanced-filters.story.tsx
@@ -1,9 +1,15 @@
 /**
  * External dependencies
  */
-import { AdvancedFilters } from '@woocommerce/components';
+import { createElement } from '@wordpress/element';

-const ORDER_STATUSES = {
+/**
+ * Internal dependencies
+ */
+import AdvancedFilters from '../';
+import type { AdvancedFilterConfig } from '../types';
+
+const ORDER_STATUSES: Record< string, string > = {
 	cancelled: 'Cancelled',
 	completed: 'Completed',
 	failed: 'Failed',
@@ -20,15 +26,15 @@ const currency = {
 	precision: 2,
 	priceFormat: '%1$s%2$s',
 	symbol: '$',
-	symbolPosition: 'left',
+	symbolPosition: 'left' as const,
 	thousandSeparator: ',',
 };
-const path = new URL( document.location ).searchParams.get( 'path' );
+const path = new URL( document.location.href ).searchParams.get( 'path' ) ?? '';
 const query = {
 	component: 'advanced-filters',
 };

-const advancedFilters = {
+const advancedFilters: AdvancedFilterConfig = {
 	title: 'Orders Match <select/> Filters',
 	filters: {
 		status: {
@@ -184,7 +190,6 @@ export const Basic = () => (
 		siteLocale={ siteLocale }
 		path={ path }
 		query={ query }
-		filterTitle="Orders"
 		config={ advancedFilters }
 		currency={ currency }
 	/>
diff --git a/packages/js/components/src/advanced-filters/test/advanced-filters.test.js b/packages/js/components/src/advanced-filters/test/advanced-filters.test.tsx
similarity index 95%
rename from packages/js/components/src/advanced-filters/test/advanced-filters.test.js
rename to packages/js/components/src/advanced-filters/test/advanced-filters.test.tsx
index 97acdc3dc26..6fcd67d5120 100644
--- a/packages/js/components/src/advanced-filters/test/advanced-filters.test.js
+++ b/packages/js/components/src/advanced-filters/test/advanced-filters.test.tsx
@@ -12,8 +12,10 @@ import { createElement } from '@wordpress/element';
  * Internal dependencies
  */
 import AdvancedFilters from '../';
+import type { AdvancedFiltersProps } from '../';
+import type { AdvancedFilterConfig } from '../types';

-const ORDER_STATUSES = {
+const ORDER_STATUSES: Record< string, string > = {
 	cancelled: 'Cancelled',
 	completed: 'Completed',
 	failed: 'Failed',
@@ -29,11 +31,11 @@ const CURRENCY = {
 	precision: 2,
 	priceFormat: '%1$s%2$s',
 	symbol: '$',
-	symbolPosition: 'left',
+	symbolPosition: 'left' as const,
 	thousandSeparator: ',',
 };

-const advancedFiltersConfig = {
+const advancedFiltersConfig: AdvancedFilterConfig = {
 	title: 'Orders Match <select/> Filters',
 	filters: {
 		status: {
@@ -201,12 +203,13 @@ const advancedFiltersConfig = {
 	},
 };

-const AdvancedFiltersComponent = ( props = null ) => (
+const AdvancedFiltersComponent = (
+	props: Partial< AdvancedFiltersProps > = {}
+) => (
 	<AdvancedFilters
 		siteLocale="en_US"
 		path=""
 		query={ { component: 'advanced-filters' } }
-		filterTitle="Orders"
 		config={ advancedFiltersConfig }
 		currency={ CURRENCY }
 		{ ...props }
diff --git a/packages/js/components/src/advanced-filters/test/search-filter.test.js b/packages/js/components/src/advanced-filters/test/search-filter.test.tsx
similarity index 84%
rename from packages/js/components/src/advanced-filters/test/search-filter.test.js
rename to packages/js/components/src/advanced-filters/test/search-filter.test.tsx
index 50e8be7ea12..6923f8dd3d8 100644
--- a/packages/js/components/src/advanced-filters/test/search-filter.test.js
+++ b/packages/js/components/src/advanced-filters/test/search-filter.test.tsx
@@ -12,8 +12,9 @@ import { createElement, createRef } from '@wordpress/element';
  * Internal dependencies
  */
 import SearchFilter from '../search-filter';
+import type { FilterConfig, FilterInput, SearchLabel } from '../types';

-const getConfig = ( getLabels ) => ( {
+const getConfig = ( getLabels: FilterInput[ 'getLabels' ] ): FilterConfig => ( {
 	labels: {
 		add: 'IP Address',
 		filter: 'Select IP addresses',
@@ -23,6 +24,7 @@ const getConfig = ( getLabels ) => ( {
 	},
 	rules: [ { value: 'includes', label: 'Includes' } ],
 	input: {
+		component: 'Search',
 		type: 'downloadIps',
 		getLabels,
 	},
@@ -34,7 +36,7 @@ describe( 'SearchFilter', () => {
 			.fn()
 			.mockResolvedValue( [ { id: '::1', label: '::1' } ] );
 		const onFilterChange = jest.fn();
-		const ref = createRef();
+		const ref = createRef< SearchFilter >();
 		const props = {
 			config: getConfig( getLabels ),
 			filter: { key: 'ip_address', rule: 'includes', value: '' },
@@ -46,7 +48,7 @@ describe( 'SearchFilter', () => {
 		);

 		act( () => {
-			ref.current.onSearchChange( [
+			ref.current?.onSearchChange( [
 				{ key: '127.0.0.1', label: '127.0.0.1' },
 			] );
 		} );
@@ -63,7 +65,7 @@ describe( 'SearchFilter', () => {
 		await waitFor( () =>
 			expect( getLabels ).toHaveBeenCalledWith( '::1', {} )
 		);
-		expect( ref.current.state.selected ).toEqual( [
+		expect( ref.current?.state.selected ).toEqual( [
 			{ id: '::1', key: '::1', label: '::1' },
 		] );

@@ -85,7 +87,7 @@ describe( 'SearchFilter', () => {
 			.fn()
 			.mockResolvedValue( [ { id: '::1', label: '::1' } ] );
 		const onFilterChange = jest.fn();
-		const ref = createRef();
+		const ref = createRef< SearchFilter >();
 		const props = {
 			config: getConfig( getLabels ),
 			filter: { key: 'ip_address', rule: 'includes', value: '' },
@@ -97,7 +99,7 @@ describe( 'SearchFilter', () => {
 		);

 		act( () => {
-			ref.current.onSearchChange( [ { key: '::1', label: '::1' } ] );
+			ref.current?.onSearchChange( [ { key: '::1', label: '::1' } ] );
 		} );
 		onFilterChange.mockClear();
 		rerender(
@@ -109,7 +111,7 @@ describe( 'SearchFilter', () => {
 		);

 		expect( getLabels ).not.toHaveBeenCalled();
-		expect( ref.current.state.selected ).toEqual( [
+		expect( ref.current?.state.selected ).toEqual( [
 			{ key: '::1', label: '::1' },
 		] );

@@ -126,7 +128,7 @@ describe( 'SearchFilter', () => {
 		const getLabels = jest
 			.fn()
 			.mockResolvedValue( [ { id: 60, label: '#60' } ] );
-		const ref = createRef();
+		const ref = createRef< SearchFilter >();
 		const props = {
 			config: getConfig( getLabels ),
 			filter: { key: 'order', rule: 'includes', value: '' },
@@ -138,7 +140,7 @@ describe( 'SearchFilter', () => {
 		);

 		act( () => {
-			ref.current.onSearchChange( [ { key: 60, label: '#60' } ] );
+			ref.current?.onSearchChange( [ { key: 60, label: '#60' } ] );
 		} );
 		rerender(
 			<SearchFilter
@@ -149,14 +151,14 @@ describe( 'SearchFilter', () => {
 		);

 		expect( getLabels ).not.toHaveBeenCalled();
-		expect( ref.current.state.selected ).toEqual( [
+		expect( ref.current?.state.selected ).toEqual( [
 			{ key: 60, label: '#60' },
 		] );
 	} );

 	test( 'does not reload labels when an array filter value matches the selection', () => {
 		const getLabels = jest.fn();
-		const ref = createRef();
+		const ref = createRef< SearchFilter >();
 		const props = {
 			config: getConfig( getLabels ),
 			filter: { key: 'order', rule: 'includes', value: '' },
@@ -168,7 +170,7 @@ describe( 'SearchFilter', () => {
 		);

 		act( () => {
-			ref.current.onSearchChange( [ { key: 60, label: '#60' } ] );
+			ref.current?.onSearchChange( [ { key: 60, label: '#60' } ] );
 		} );
 		rerender(
 			<SearchFilter
@@ -206,7 +208,7 @@ describe( 'SearchFilter', () => {
 		const getLabels = jest
 			.fn()
 			.mockResolvedValue( [ { id: 60, label: '#60' } ] );
-		const ref = createRef();
+		const ref = createRef< SearchFilter >();
 		const props = {
 			config: getConfig( getLabels ),
 			filter: { key: 'order', rule: 'includes', value: '' },
@@ -231,19 +233,19 @@ describe( 'SearchFilter', () => {
 	} );

 	test( 'ignores stale label responses', async () => {
-		let resolveFirstRequest;
-		let resolveSecondRequest;
-		const firstRequest = new Promise( ( resolve ) => {
+		let resolveFirstRequest: ( labels: SearchLabel[] ) => void = () => {};
+		let resolveSecondRequest: ( labels: SearchLabel[] ) => void = () => {};
+		const firstRequest = new Promise< SearchLabel[] >( ( resolve ) => {
 			resolveFirstRequest = resolve;
 		} );
-		const secondRequest = new Promise( ( resolve ) => {
+		const secondRequest = new Promise< SearchLabel[] >( ( resolve ) => {
 			resolveSecondRequest = resolve;
 		} );
 		const getLabels = jest
 			.fn()
 			.mockReturnValueOnce( firstRequest )
 			.mockReturnValueOnce( secondRequest );
-		const ref = createRef();
+		const ref = createRef< SearchFilter >();
 		const props = {
 			config: getConfig( getLabels ),
 			filter: { key: 'ip_address', rule: 'includes', value: '' },
@@ -278,7 +280,7 @@ describe( 'SearchFilter', () => {
 			await firstRequest;
 		} );

-		expect( ref.current.state.selected ).toEqual( [
+		expect( ref.current?.state.selected ).toEqual( [
 			{ id: '127.0.0.1', key: '127.0.0.1', label: '127.0.0.1' },
 		] );
 	} );
diff --git a/packages/js/components/src/advanced-filters/test/utils.js b/packages/js/components/src/advanced-filters/test/utils.tsx
similarity index 100%
rename from packages/js/components/src/advanced-filters/test/utils.js
rename to packages/js/components/src/advanced-filters/test/utils.tsx
diff --git a/packages/js/components/src/advanced-filters/types.ts b/packages/js/components/src/advanced-filters/types.ts
new file mode 100644
index 00000000000..f48812d23f5
--- /dev/null
+++ b/packages/js/components/src/advanced-filters/types.ts
@@ -0,0 +1,120 @@
+/**
+ * External dependencies
+ */
+import type { CurrencyConfig } from '@woocommerce/currency';
+
+/**
+ * Internal dependencies
+ */
+import type { AutoCompleter } from '../search/autocompleters';
+
+/**
+ * Parsed URL query. `allowMultiple` filters serialize as nested arrays
+ * (`attribute_is[0][0]=1`), so values are not always plain strings.
+ */
+export type Query = Record<
+	string,
+	string | string[] | string[][] | undefined
+>;
+
+export type FilterRule = {
+	value: string;
+	label: string;
+};
+
+export type FilterOption = {
+	value: string;
+	label: string;
+};
+
+export type FilterLabels = {
+	add: string;
+	remove?: string;
+	rule?: string;
+	title: string;
+	filter?: string;
+	placeholder?: string;
+};
+
+/**
+ * A label resolved for a selected search value, as returned by `input.getLabels`.
+ */
+export type SearchLabel = {
+	id?: string | number;
+	key?: string | number;
+	label: string;
+};
+
+export type FilterInput = {
+	component: string;
+	type?: string;
+	options?: FilterOption[];
+	defaultOption?: string;
+	getOptions?: () => Promise< FilterOption[] >;
+	getLabels?: ( value: string, query?: Query ) => Promise< SearchLabel[] >;
+	autocompleter?: AutoCompleter;
+};
+
+export type FilterConfig = {
+	labels: FilterLabels;
+	rules?: FilterRule[];
+	input: FilterInput;
+	allowMultiple?: boolean;
+};
+
+export type AdvancedFilterConfig = {
+	title: string;
+	filters: Record< string, FilterConfig >;
+};
+
+/**
+ * Range filters hold `[ start, end ]` while either end may still be unset.
+ */
+export type ActiveFilterValue =
+	| string
+	| number
+	| Array< string | number | null | undefined >
+	| null;
+
+export type ActiveFilter = {
+	key: string;
+	rule?: string;
+	value?: ActiveFilterValue;
+	instance?: number;
+};
+
+export type FilterChange = {
+	property: 'rule' | 'value';
+	value: ActiveFilterValue | undefined;
+	shouldResetValue?: boolean;
+};
+
+export type OnFilterChange = ( change: FilterChange ) => void;
+
+export type AdvancedFilterAction =
+	| 'add'
+	| 'remove'
+	| 'match'
+	| 'filter'
+	| 'clear_all';
+
+/**
+ * `getCurrencyConfig()` returns `symbolPosition` as a plain string, so this stays
+ * wider than `CurrencyProps` to accept what the site actually passes.
+ */
+export type Currency = Omit< CurrencyConfig, 'symbolPosition' > & {
+	symbol: string;
+	symbolPosition: string;
+};
+
+/**
+ * Props shared by every single-filter component rendered by `AdvancedFilterItem`.
+ */
+export type FilterComponentProps = {
+	className?: string;
+	config: FilterConfig;
+	filter: ActiveFilter;
+	isEnglish?: boolean;
+	onFilterChange: OnFilterChange;
+	query?: Query;
+};
diff --git a/packages/js/components/src/advanced-filters/utils.js b/packages/js/components/src/advanced-filters/utils.ts
similarity index 83%
rename from packages/js/components/src/advanced-filters/utils.js
rename to packages/js/components/src/advanced-filters/utils.ts
index d74bf26e030..701b0cee9b3 100644
--- a/packages/js/components/src/advanced-filters/utils.js
+++ b/packages/js/components/src/advanced-filters/utils.ts
@@ -3,7 +3,8 @@
  */
 import { isArray, isNumber, isString } from 'lodash';
 import deprecated from '@wordpress/deprecated';
-import { createInterpolateElement } from '@wordpress/element';
+import { createInterpolateElement, isValidElement } from '@wordpress/element';
+import type { ReactElement, ReactNode } from 'react';

 /**
  * DOM Node.textContent for React components
@@ -13,15 +14,18 @@ import { createInterpolateElement } from '@wordpress/element';
  *
  * @return {string} concatenated text content of all nodes
  */
-export function textContent( components ) {
+export function textContent( components: ReactNode ) {
 	let text = '';

-	const toText = ( component ) => {
+	const toText = ( component: ReactNode ) => {
 		if ( isString( component ) || isNumber( component ) ) {
 			text += component;
 		} else if ( isArray( component ) ) {
 			component.forEach( toText );
-		} else if ( component && component.props ) {
+		} else if (
+			isValidElement< { children?: ReactNode } >( component ) &&
+			component.props
+		) {
 			const { children } = component.props;

 			if ( isArray( children ) ) {
@@ -47,12 +51,12 @@ export function textContent( components ) {
  *
  * @return {string}  Fixed interpolation string.
  */
-export function getInterpolatedString( interpolatedString ) {
+export function getInterpolatedString( interpolatedString: string ) {
 	const regex = /(\{\{)(\/?\s*\w+\s*\/?)(\}\})/g;

 	const replacedString = interpolatedString.replaceAll(
 		regex,
-		( match, p1, p2 ) => {
+		( match: string, p1: string, p2: string ) => {
 			const inner = p2.trim();
 			let replacement;
 			if ( inner.startsWith( '/' ) ) {
@@ -94,8 +98,8 @@ export function getInterpolatedString( interpolatedString ) {
  * @return {Element} A React element that is the result of applying the transformation.
  */
 export function backwardsCompatibleCreateInterpolateElement(
-	interpolatedString,
-	conversionMap
+	interpolatedString: string,
+	conversionMap: Record< string, ReactElement >
 ) {
 	return createInterpolateElement(
 		getInterpolatedString( interpolatedString ),
diff --git a/packages/js/components/src/index.ts b/packages/js/components/src/index.ts
index ff8f208e25d..78b7e6b899c 100644
--- a/packages/js/components/src/index.ts
+++ b/packages/js/components/src/index.ts
@@ -1,5 +1,19 @@
 export { default as AbbreviatedCard } from './abbreviated-card';
 export { default as AdvancedFilters } from './advanced-filters';
+export type {
+	ActiveFilter,
+	ActiveFilterValue,
+	AdvancedFilterAction,
+	AdvancedFilterConfig,
+	AdvancedFiltersProps,
+	FilterChange,
+	FilterConfig,
+	FilterInput,
+	FilterLabels,
+	FilterOption,
+	FilterRule,
+	Query,
+} from './advanced-filters';
 export * from './analytics';
 export { default as AnimationSlider } from './animation-slider';
 export { default as Chart } from './chart';
diff --git a/plugins/woocommerce/changelog/dev-report-header-advanced-filters-type b/plugins/woocommerce/changelog/dev-report-header-advanced-filters-type
new file mode 100644
index 00000000000..811b74f7296
--- /dev/null
+++ b/plugins/woocommerce/changelog/dev-report-header-advanced-filters-type
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+Comment: Type the ReportHeader advancedFilters prop with AdvancedFilterConfig from @woocommerce/components.
+
diff --git a/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx b/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx
index 3dd1fa9bc7f..9f898f39174 100644
--- a/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx
+++ b/plugins/woocommerce/client/admin/client/analytics/components/report-header/report-header.tsx
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import type { AdvancedFilterConfig } from '@woocommerce/components';
+
 /**
  * Internal dependencies
  */
@@ -10,7 +15,7 @@ interface ReportHeaderProps {
 	/**
 	 * Config option passed through to `AdvancedFilters`
 	 */
-	advancedFilters?: object;
+	advancedFilters?: AdvancedFilterConfig;
 	/**
 	 * Config option passed through to `FilterPicker`
 	 */