Skip to content

chore(types): enable exactOptionalPropertyTypes support#3719

Open
YevheniiKotyrlo wants to merge 5 commits intosoftware-mansion:mainfrom
YevheniiKotyrlo:fix/exact-optional-property-types
Open

chore(types): enable exactOptionalPropertyTypes support#3719
YevheniiKotyrlo wants to merge 5 commits intosoftware-mansion:mainfrom
YevheniiKotyrlo:fix/exact-optional-property-types

Conversation

@YevheniiKotyrlo
Copy link
Copy Markdown

@YevheniiKotyrlo YevheniiKotyrlo commented Mar 3, 2026

Description

This PR enables TypeScript's exactOptionalPropertyTypes flag in the repo and adds | undefined to every non-required prop so the flag stays on cleanly.

exactOptionalPropertyTypes is a strict TypeScript flag (not included in strict: true) that distinguishes between "property is missing" and "property is explicitly undefined". When enabled, prop?: T means the property can be omitted but cannot be set to undefined — the fix is prop?: T | undefined.

This matters for downstream projects that enable this flag — without this change, they get type errors when passing destructured props from react-native-screens components. This is currently blocking react-navigation from enabling the flag — 17 of 22 @ts-expect-error directives in that PR come from react-native-screens types.

Changes

The initial commit fixed only the 13 concrete eopt errors (14 after the later Tabs refactor). After internal discussion, @kligarski broadened the scope to every non-required prop across the codebase for a consistent public API — commits 14c3927d8 ("add undefined to all non-required props") and bebd04cba ("remove undefined after CT.WithDefault to satisfy codegen"). The merge commit bf4c49ab3 integrates latest main and resolves the 4 conflicts from the ScrollViewMarker migration (#3895) and controlNavigationStateInJS removal (#3888).

Current diff: 41 files, +722/-605

Category Files Note
Root 2 tsconfig.json (enables flag), src/types.tsx (all non-required props in ScreenProps, GestureProps, etc.)
Public component types 14 .types.ts SafeAreaView, ScrollViewMarker, Split, Stack, Tabs, TabsHost, TabsScreen, TabsAccessory, TabsBottomAccessory, etc.
Component helpers 10 .tsx / .ts parse/helper return types in DebugContainer, FullWindowOverlay, ScreenFooter, ScreenStackHeaderConfig.web, ScreenStackItem, SearchBar, TabsScreen.android, TabsScreen.ios, useTabsScreen, useTabsHost
Top-level Fabric specs 6 ModalScreenNativeComponent, ScreenNativeComponent, ScreenStackHeaderConfigNativeComponent, ScreenStackHeaderSubviewNativeComponent, ScreenStackNativeComponent, SearchBarNativeComponent
Nested Fabric specs 9 gamma/split/* (2), gamma/stack/StackScreenNativeComponent, safe-area/SafeAreaViewNativeComponent, tabs/* (5: TabsBottomAccessory, TabsHostAndroid/IOS, TabsScreenAndroid/IOS)

All changes are type annotations only — zero runtime changes.

Codegen compatibility

The src/fabric/ spec files are consumed by @react-native/codegen. Adding | undefined to optional properties is safe — the codegen parser (parseTopLevelType.js) explicitly strips undefined from union types and treats the result identically to prop?: T.

Exception: CT.WithDefault<T, V> props are NOT modified — WithDefault already expands to T | undefined | null so it's already eopt-compatible, and combining it with | undefined crashes the codegen parser ("WithDefault<> is optional and does not need to be marked as optional"). Commit bebd04cba removes the stray | undefined additions after any CT.WithDefault<> for exactly this reason.

I verified the codegen schema output is byte-identical between main and this branch across all 23 current Fabric spec files:

FABRIC_FILES=$(find src/fabric -name "*NativeComponent.ts" -type f | tr '\n' ' ')
node node_modules/@react-native/codegen/lib/cli/combine/combine-js-to-schema-cli.js \
  /tmp/schema.json --libraryName rnscreens $FABRIC_FILES

Run on both origin/main and this branch, diff -q exits 0.

Test plan

  • yarn check-types passes with 0 errors (with exactOptionalPropertyTypes enabled)
  • yarn lint-js passes with 0 errors and the same 18 pre-existing warnings as origin/main
  • yarn prettier --check src/ is clean
  • Codegen schema byte-identical to main across all 23 src/fabric/**NativeComponent.ts files

Checklist

  • For API changes, updated relevant public types.
  • Ensured that CI passes (pending maintainer approval for fork-PR workflow runs)

@kkafar kkafar requested review from Copilot and kkafar March 3, 2026 21:22
@kkafar kkafar self-assigned this Mar 3, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables TypeScript’s exactOptionalPropertyTypes across the repo and updates public and Fabric/codegen-facing type annotations to distinguish “missing” vs “explicitly undefined” optional props, improving compatibility for downstream consumers that enable the flag.

Changes:

  • Enabled exactOptionalPropertyTypes in tsconfig.json.
  • Added | undefined to optional props across public types (src/types.tsx, SafeAreaView types) and internal Tabs parsing helper return types.
  • Updated Fabric codegen spec types under src/fabric/** to be compatible with exactOptionalPropertyTypes while preserving codegen behavior.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tsconfig.json Enables exactOptionalPropertyTypes for the project.
src/types.tsx Adjusts several ScreenProps and GestureProps optional fields to allow explicit undefined.
src/components/safe-area/SafeAreaView.types.ts Updates edges prop type to allow explicit undefined.
src/components/tabs/TabsScreen.tsx Updates parseIconsToNativeProps return type optional fields to allow explicit undefined.
src/fabric/ScreenNativeComponent.ts Updates optional event handler NativeProps to allow explicit undefined.
src/fabric/ModalScreenNativeComponent.ts Updates optional event handler NativeProps to allow explicit undefined.
src/fabric/ScreenStackNativeComponent.ts Updates onFinishTransitioning to allow explicit undefined.
src/fabric/ScreenStackHeaderConfigNativeComponent.ts Updates optional event handlers / arrays to allow explicit undefined.
src/fabric/safe-area/SafeAreaViewNativeComponent.ts Updates edges NativeProp type to allow explicit undefined.
src/fabric/tabs/TabsScreenNativeComponent.ts Updates tab appearance-related optional fields to allow explicit undefined.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/components/tabs/TabsScreen.tsx Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

src/types.tsx:168

  • With exactOptionalPropertyTypes enabled, optional props like active?: ... are still not assignable from a destructured/forwarded value of type ... | undefined (e.g. const { active } = props; <Screen active={active} />). In this hunk only activityState/shouldFreeze were updated; consider updating active (and other optional ScreenProps fields that can be forwarded) to ?: T | undefined as well to fully address downstream eopt compatibility.
export interface ScreenProps extends ViewProps {
  active?: 0 | 1 | Animated.AnimatedInterpolation<number>;
  activityState?:
    | 0
    | 1
    | 2
    | Animated.AnimatedInterpolation<number>
    | undefined;
  /**
   * Boolean indicating that the screen should be frozen with `react-freeze`.
   */
  shouldFreeze?: boolean | undefined;
  children?: React.ReactNode;
  /**
   * Boolean indicating that swipe dismissal should trigger animation provided by `stackAnimation`. Defaults to `false`.
   *
   * @platform ios
   */
  customAnimationOnSwipe?: boolean;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/components/safe-area/SafeAreaView.types.ts Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@YevheniiKotyrlo YevheniiKotyrlo force-pushed the fix/exact-optional-property-types branch from 1db290d to cbab010 Compare March 14, 2026 21:46
@kligarski
Copy link
Copy Markdown
Contributor

kligarski commented Apr 15, 2026

Hi, @YevheniiKotyrlo. Thank you for the PR.

After internal discussion we decided to use prop?: T | undefined for all non-required props to provide consistent API, not only those that resulted in TS error in the current state. If git allows, I'll push some changes to finalize the PR. I hope you don't mind.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 41 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, we use in in ScreenStackHeaderConfig.tsx which is sensitive to not defined vs undefined but we should be good here. buttonId and menuId are internal props set to some value and items is a required property.

@@ -3,12 +3,14 @@ import type { ScrollEdgeEffect } from '../../shared/types';

export interface ScrollViewMarkerProps {
children: ViewProps['children'];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can consider adding undefined to this prop or removing nullability from ViewProps['children'].

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

children is required here (no ?), so exactOptionalPropertyTypes doesn't actually constrain it — the flag only affects optional props. ViewProps['children'] is React.ReactNode | undefined (and React.ReactNode itself already includes null | undefined), so forwarded values stay assignable to this required prop regardless. Happy to leave as-is for this PR, or to make it optional (children?: ViewProps['children']) for consistency with the rest of the interface — let me know which you prefer.

…operty-types

# Conflicts:
#	src/components/gamma/scroll-view-marker/ScrollViewMarker.types.ts
#	src/components/tabs/host/TabsHost.types.ts
#	src/components/tabs/host/useTabsHost.ts
#	src/components/tabs/screen/TabsScreen.ios.types.ts
@YevheniiKotyrlo
Copy link
Copy Markdown
Author

Thanks @kligarski — really appreciate you taking it further. Broadening to all non-required props is the right call for API consistency, no objections at all.

Just pushed a merge with latest main (bf4c49ab3) to unblock merge status. The 4 conflicts came from the ScrollViewMarker migration (#3895) and the controlNavigationStateInJS removal (#3888) — resolved by keeping your | undefined markers in ScrollViewMarker.types.ts under the new JSDoc, and dropping the now-deleted props from TabsHost.types.ts / useTabsHost.ts / TabsScreen.ios.types.ts.

Quality gates all pass locally: yarn check-types (0 errors), yarn lint-js (0 errors, 18 warnings — matches origin/main exactly), yarn prettier --check src/ (clean), and codegen schema byte-identical to main across all 23 src/fabric/**NativeComponent.ts files. Good that you caught the WithDefault<> codegen trap — that's exactly the pitfall I flagged in the original PR description and avoided up front; glad your broader-scope commit handled it the same way (skip | undefined after any CT.WithDefault<>).

On the in-operator audit in ScreenStackHeaderConfig.tsx — agreed the three usages are safe: buttonId/menuId are internal props always set to a concrete value, and items is required. Good that you checked it explicitly; that's exactly the subtlety that tends to bite under exactOptionalPropertyTypes (explicit undefined vs missing key), so having it confirmed up front is worth it.

Also — I've refreshed the PR description to match the broader post-takeover scope (41 files, not the original 10) and to update the codegen verification command against the current Fabric file layout. The old description was stale.

CI workflows are currently in action_required state (fork PRs need a maintainer click to run) — could you approve the run when you get a chance? Ready for final review whenever you have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants