diff --git a/docs/react/overlays.md b/docs/react/overlays.md
index 8de7b6c971..578d8e8ed2 100644
--- a/docs/react/overlays.md
+++ b/docs/react/overlays.md
@@ -49,11 +49,21 @@ showAlert({
Overlay hooks that display additional custom components as part of their markup, such as [modals](https://ionicframework.com/docs/api/modal) and [popovers](https://ionicframework.com/docs/api/popover), take in a couple of additional parameters when initializing their hooks. The first parameter is the component you want your overlay to display, and the second is an object of additional props you want to pass into the component when it gets constructed:
```tsx
-const [present, dismiss] = useIonModal(({ name }) =>
Hello {name}.
, {
+const [present, dismiss] = useIonModal(({ name }: { name: string }) => Hello {name}.
, {
name: 'Dave',
});
```
+That second argument, `componentProps`, is type checked against the props the component declares, so passing a prop the component does not accept, or omitting one it requires, is a compile error. The component must declare its props type; the hook does not infer it from `componentProps`. That can be an annotation on the parameter, as above, or on the component itself:
+
+```tsx
+const Greeting: React.FC<{ name: string }> = ({ name }) => Hello {name}.
;
+
+const [present, dismiss] = useIonModal(Greeting, { name: 'Dave' });
+```
+
+Passing a JSX element instead of a component binds the props to the element, and `componentProps` is not type checked.
+
## Overlay Components
Overlays can also be displayed by using components from `@ionic/react`. The components take a `isOpen` prop that you provide to control if the overlay is currently being displayed or not. When `isOpen` switches from true to false (and vise versa), Ionic will open/close the overlay with the appropriate animation. You can also supply any other additional config options as props to the overlay:
diff --git a/docs/reference/support.md b/docs/reference/support.md
index 944bc112f6..d15fdef52e 100644
--- a/docs/reference/support.md
+++ b/docs/reference/support.md
@@ -56,7 +56,7 @@ The Ionic team has compiled a set of recommendations for using the Ionic Framewo
[^1]: Angular 14.x supported starting in Ionic v6.1.9. Angular 15.x supported starting in Ionic v6.3.6.
[^2]: Angular 17.x supported starting in Ionic v7.5.4.
[^3]: Angular 18.x supported starting in Ionic v8.2.0.
-[^4]: Ionic v9 supports TypeScript 5.4+ for compatibility with Angular 18. Using Angular 21 requires TypeScript 5.9 or later, and Angular 22 requires TypeScript 6.0 or later, per Angular's own requirements.
+[^4]: `@ionic/angular` and `@ionic/react` require TypeScript 5.4+. Using Angular 21 requires TypeScript 5.9 or later, and Angular 22 requires TypeScript 6.0 or later, per Angular's own requirements.
**Angular 13+ Support On Older Versions of iOS**
@@ -68,7 +68,7 @@ Note that later versions of Ionic do not support iOS 13; refer to the [mobile su
| Framework | Required React Version | TypeScript |
| :-------: | :--------------------: | :--------: |
-| v9 | v18+ | 3.7+ |
+| v9 | v18+ | 5.4+[^4] |
| v8 | v17+ | 3.7+ |
| v7 | v17+ | 3.7+ |
| v6 | v17+ | 3.7+ |
diff --git a/docs/updating/9-0.md b/docs/updating/9-0.md
index 6c4081fb71..b2b5efaf5f 100644
--- a/docs/updating/9-0.md
+++ b/docs/updating/9-0.md
@@ -30,6 +30,7 @@ Useful options:
- `--dry-run` reports what would change without writing anything.
- `--check` reports only and exits non-zero if any migration is still pending, for use in CI.
+- `--experimental` also runs experimental migrations, which apply fixes that need review.
- `--force` writes even if the working tree is dirty or the project is not a git repository.
- `--no-format` skips the Prettier pass over the changed files.
- `--no-install` skips the dependency reinstall.
@@ -180,6 +181,42 @@ npm install react@latest react-dom@latest
npm install @ionic/react@latest @ionic/react-router@latest
```
+#### TypeScript {/* #react-typescript */}
+
+The `@ionic/react` package requires TypeScript 5.4 or later. Its type definitions use `NoInfer`, which TypeScript added in 5.4.
+
+#### Typed Overlay Hook Props
+
+The `useIonModal` and `useIonPopover` hooks now type `componentProps` against the component they are given, instead of accepting `any`. Passing props that do not match the component is a compile error, and `componentProps` is required when the component declares required props:
+
+```tsx
+const Modal: React.FC<{ title: string }> = ({ title }) => {title}
;
+
+// Error: componentProps is required because 'title' is required
+useIonModal(Modal);
+
+// Error: 'subtitle' does not exist on the component's props
+useIonModal(Modal, { title: 'Hello', subtitle: 'Nope' });
+
+// Correct
+const [present, dismiss] = useIonModal(Modal, { title: 'Hello' });
+```
+
+Omitting `componentProps` altogether rules out the component signature, so TypeScript reports a mismatch against the JSX element signature rather than naming the missing prop.
+
+This surfaces existing mistakes at build time rather than at runtime, so an app that was passing incorrect props will see new type errors. Fix the call sites to match the component. Components typed as `FC` stay permissive, so an app whose overlay components are untyped needs no changes. To pin the props type instead of relying on inference, pass it as a type argument: `useIonModal(Component, props)`.
+
+The prop types are inferred from the component, not from `componentProps`. A component defined inline needs its props annotated:
+
+```diff
+- const [present, dismiss] = useIonModal(({ name }) => Hello {name}.
, { name: 'Dave' });
++ const [present, dismiss] = useIonModal(({ name }: { name: string }) => Hello {name}.
, { name: 'Dave' });
+```
+
+Running the [migration tool](#automated-migration) with `npx @ionic/migrate --experimental` can annotate the inline case for you. The remaining call sites are reported rather than rewritten, since the correct fix depends on what the component is meant to accept.
+
+Passing a JSX element rather than a component behaves as before: props are bound to the element and `componentProps` is not type checked.
+
### React Router
1. Ionic 9 supports React Router 6. Update to version 6 of React Router:
diff --git a/static/usage/v9/modal/controller/react.md b/static/usage/v9/modal/controller/react.md
index 0cb92e7ad3..5f1d0033c9 100644
--- a/static/usage/v9/modal/controller/react.md
+++ b/static/usage/v9/modal/controller/react.md
@@ -44,7 +44,7 @@ const ModalExample = ({ dismiss }: { dismiss: (data?: string | null | undefined
function Example() {
const [present, dismiss] = useIonModal(ModalExample, {
- dismiss: (data: string, role: string) => dismiss(data, role),
+ dismiss: (data?: string | number | null, role?: string) => dismiss(data, role),
});
const [message, setMessage] = useState('This modal example uses the modalController to present and dismiss modals.');
diff --git a/static/usage/v9/popover/presenting/controller/react.md b/static/usage/v9/popover/presenting/controller/react.md
index d23a58cbef..48043ab57e 100644
--- a/static/usage/v9/popover/presenting/controller/react.md
+++ b/static/usage/v9/popover/presenting/controller/react.md
@@ -5,9 +5,7 @@ import { IonButton, IonContent, useIonPopover } from '@ionic/react';
const Popover = () => Hello World!;
function Example() {
- const [present, dismiss] = useIonPopover(Popover, {
- onDismiss: (data: any, role: string) => dismiss(data, role),
- });
+ const [present] = useIonPopover(Popover);
return (