Skip to content

Commit 58c4641

Browse files
Enable exactOptionalPropertyTypes support (#4012)
## Description This PR enables TypeScript's [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes) flag in `react-native-gesture-handler` and fixes all 60 resulting type errors across 22 files. `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 using gesture-handler components. This is currently blocking [react-navigation from enabling the flag](react-navigation/react-navigation#12995). ### What changed Three categories of fixes across v2 (class-based), v3 (hooks-based), and web handler code: 1. **Type declarations** — Added `| undefined` to optional properties in interfaces and types (`HitSlop`, `CommonGestureConfig`, `BaseGestureHandlerProps`, `ButtonProps`, `ExternalRelations`, `VirtualChild`, `IGestureHandler`, etc.) 2. **Class property types** — Updated class property declarations in `GestureHandler.ts` to include `| undefined` for optional config properties (`_testID`, `hitSlop`, `mouseButton`, `_activeCursor`, etc.) 3. **`WithSharedValueRecursive` mapped type** — Fixed the boolean special case in `ReanimatedTypes.ts` to preserve `undefined` from optional properties using `| Extract<T[K], undefined>` All changes are type annotations only — zero runtime changes. ### Codegen compatibility The `src/specs/` files are consumed by `@react-native/codegen`. One spec file was modified (`RNGestureHandlerDetectorNativeComponent.ts` — 7 `DirectEventHandler` event props). Adding `| undefined` to these is safe — the codegen parser ([`parseTopLevelType.js`](https://github.com/facebook/react-native/blob/main/packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js)) explicitly strips `undefined` from union types and treats the result identically to `prop?: T`. I verified that the codegen schema output is **byte-identical** between `main` and this branch: ```bash node node_modules/@react-native/codegen/lib/cli/combine/combine-js-to-schema-cli.js \ /tmp/schema.json --libraryName rngesturehandler_codegen \ packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts \ packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts ``` ## Test plan All quality gates pass: - `yarn workspace react-native-gesture-handler ts-check` — 0 errors - `yarn workspace react-native-gesture-handler lint-js` — 0 errors - `yarn workspace react-native-gesture-handler test` — 5 suites, 41 tests pass - `yarn workspace react-native-gesture-handler circular-dependency-check` — pass
1 parent edef276 commit 58c4641

22 files changed

Lines changed: 191 additions & 166 deletions

packages/react-native-gesture-handler/src/components/GestureButtonsProps.ts

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,42 +18,42 @@ export interface LegacyRawButtonProps
1818
* Defines if more than one button could be pressed simultaneously. By default
1919
* set true.
2020
*/
21-
exclusive?: boolean;
21+
exclusive?: boolean | undefined;
2222
// TODO: we should transform props in `createNativeWrapper`
2323
/**
2424
* Android only.
2525
*
2626
* Defines color of native ripple animation used since API level 21.
2727
*/
28-
rippleColor?: number | ColorValue | null;
28+
rippleColor?: number | ColorValue | null | undefined;
2929

3030
/**
3131
* Android only.
3232
*
3333
* Defines radius of native ripple animation used since API level 21.
3434
*/
35-
rippleRadius?: number | null;
35+
rippleRadius?: number | null | undefined;
3636

3737
/**
3838
* Android only.
3939
*
4040
* Set this to true if you want the ripple animation to render outside the view bounds.
4141
*/
42-
borderless?: boolean;
42+
borderless?: boolean | undefined;
4343

4444
/**
4545
* Android only.
4646
*
4747
* Defines whether the ripple animation should be drawn on the foreground of the view.
4848
*/
49-
foreground?: boolean;
49+
foreground?: boolean | undefined;
5050

5151
/**
5252
* Android only.
5353
*
5454
* Set this to true if you don't want the system to play sound when the button is pressed.
5555
*/
56-
touchSoundDisabled?: boolean;
56+
touchSoundDisabled?: boolean | undefined;
5757

5858
/**
5959
* Style object, use it to set additional styles.
@@ -70,31 +70,31 @@ export interface LegacyRawButtonProps
7070
* @deprecated test-only props are deprecated and will be removed in the future.
7171
*/
7272
// eslint-disable-next-line @typescript-eslint/ban-types
73-
testOnly_onPress?: Function | null;
73+
testOnly_onPress?: Function | null | undefined;
7474

7575
/**
7676
* Used for testing-library compatibility, not passed to the native component.
7777
* @deprecated test-only props are deprecated and will be removed in the future.
7878
*/
7979
// eslint-disable-next-line @typescript-eslint/ban-types
80-
testOnly_onPressIn?: Function | null;
80+
testOnly_onPressIn?: Function | null | undefined;
8181

8282
/**
8383
* Used for testing-library compatibility, not passed to the native component.
8484
* @deprecated test-only props are deprecated and will be removed in the future.
8585
*/
8686
// eslint-disable-next-line @typescript-eslint/ban-types
87-
testOnly_onPressOut?: Function | null;
87+
testOnly_onPressOut?: Function | null | undefined;
8888

8989
/**
9090
* Used for testing-library compatibility, not passed to the native component.
9191
* @deprecated test-only props are deprecated and will be removed in the future.
9292
*/
9393
// eslint-disable-next-line @typescript-eslint/ban-types
94-
testOnly_onLongPress?: Function | null;
94+
testOnly_onLongPress?: Function | null | undefined;
9595
}
9696
interface ButtonWithRefProps {
97-
innerRef?: React.ForwardedRef<React.ComponentType<any>>;
97+
innerRef?: React.ForwardedRef<React.ComponentType<any>> | undefined;
9898
}
9999

100100
/**
@@ -105,28 +105,28 @@ export interface LegacyBaseButtonProps extends LegacyRawButtonProps {
105105
* Called when the button gets pressed (analogous to `onPress` in
106106
* `TouchableHighlight` from RN core).
107107
*/
108-
onPress?: (pointerInside: boolean) => void;
108+
onPress?: ((pointerInside: boolean) => void) | undefined;
109109

110110
/**
111111
* Called when the button gets pressed and is held for `delayLongPress`
112112
* milliseconds.
113113
*/
114-
onLongPress?: () => void;
114+
onLongPress?: (() => void) | undefined;
115115

116116
/**
117117
* Called when button changes from inactive to active and vice versa. It
118118
* passes active state as a boolean variable as a first parameter for that
119119
* method.
120120
*/
121-
onActiveStateChange?: (active: boolean) => void;
121+
onActiveStateChange?: ((active: boolean) => void) | undefined;
122122
style?: StyleProp<ViewStyle>;
123-
testID?: string;
123+
testID?: string | undefined;
124124

125125
/**
126126
* Delay, in milliseconds, after which the `onLongPress` callback gets called.
127127
* Defaults to 600.
128128
*/
129-
delayLongPress?: number;
129+
delayLongPress?: number | undefined;
130130
}
131131
export interface BaseButtonWithRefProps
132132
extends LegacyBaseButtonProps,
@@ -139,14 +139,14 @@ export interface LegacyRectButtonProps extends LegacyBaseButtonProps {
139139
/**
140140
* Background color that will be dimmed when button is in active state.
141141
*/
142-
underlayColor?: string;
142+
underlayColor?: string | undefined;
143143

144144
/**
145145
* iOS only.
146146
*
147147
* Opacity applied to the underlay when button is in active state.
148148
*/
149-
activeOpacity?: number;
149+
activeOpacity?: number | undefined;
150150
}
151151
export interface RectButtonWithRefProps
152152
extends LegacyRectButtonProps,
@@ -162,7 +162,7 @@ export interface LegacyBorderlessButtonProps extends LegacyBaseButtonProps {
162162
*
163163
* Opacity applied to the button when it is in an active state.
164164
*/
165-
activeOpacity?: number;
165+
activeOpacity?: number | undefined;
166166
}
167167
export interface BorderlessButtonWithRefProps
168168
extends LegacyBorderlessButtonProps,

packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,48 +19,48 @@ export interface ButtonProps extends ViewProps, AccessibilityProps {
1919
/**
2020
* Defines if buttons should respond to touches. By default set to true.
2121
*/
22-
enabled?: boolean;
22+
enabled?: boolean | undefined;
2323

2424
/**
2525
* Defines if more than one button could be pressed simultaneously. By default
2626
* set true.
2727
*/
28-
exclusive?: boolean;
28+
exclusive?: boolean | undefined;
2929

3030
/**
3131
* Android only.
3232
*
3333
* Defines color of native ripple animation used since API level 21.
3434
*/
35-
rippleColor?: number | ColorValue | null;
35+
rippleColor?: number | ColorValue | null | undefined;
3636

3737
/**
3838
* Android only.
3939
*
4040
* Defines radius of native ripple animation used since API level 21.
4141
*/
42-
rippleRadius?: number | null;
42+
rippleRadius?: number | null | undefined;
4343

4444
/**
4545
* Android only.
4646
*
4747
* Set this to true if you want the ripple animation to render outside the view bounds.
4848
*/
49-
borderless?: boolean;
49+
borderless?: boolean | undefined;
5050

5151
/**
5252
* Android only.
5353
*
5454
* Defines whether the ripple animation should be drawn on the foreground of the view.
5555
*/
56-
foreground?: boolean;
56+
foreground?: boolean | undefined;
5757

5858
/**
5959
* Android only.
6060
*
6161
* Set this to true if you don't want the system to play sound when the button is pressed.
6262
*/
63-
touchSoundDisabled?: boolean;
63+
touchSoundDisabled?: boolean | undefined;
6464

6565
/**
6666
* Style object, use it to set additional styles.
@@ -77,28 +77,28 @@ export interface ButtonProps extends ViewProps, AccessibilityProps {
7777
* @deprecated test-only props are deprecated and will be removed in the future.
7878
*/
7979
// eslint-disable-next-line @typescript-eslint/ban-types
80-
testOnly_onPress?: Function | null;
80+
testOnly_onPress?: Function | null | undefined;
8181

8282
/**
8383
* Used for testing-library compatibility, not passed to the native component.
8484
* @deprecated test-only props are deprecated and will be removed in the future.
8585
*/
8686
// eslint-disable-next-line @typescript-eslint/ban-types
87-
testOnly_onPressIn?: Function | null;
87+
testOnly_onPressIn?: Function | null | undefined;
8888

8989
/**
9090
* Used for testing-library compatibility, not passed to the native component.
9191
* @deprecated test-only props are deprecated and will be removed in the future.
9292
*/
9393
// eslint-disable-next-line @typescript-eslint/ban-types
94-
testOnly_onPressOut?: Function | null;
94+
testOnly_onPressOut?: Function | null | undefined;
9595

9696
/**
9797
* Used for testing-library compatibility, not passed to the native component.
9898
* @deprecated test-only props are deprecated and will be removed in the future.
9999
*/
100100
// eslint-disable-next-line @typescript-eslint/ban-types
101-
testOnly_onLongPress?: Function | null;
101+
testOnly_onLongPress?: Function | null | undefined;
102102
}
103103

104104
const ButtonComponent =

packages/react-native-gesture-handler/src/components/Pressable/PressableProps.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export type InnerPressableEvent = {
2626
target: number;
2727
timestamp: number;
2828
touches: InnerPressableEvent[];
29-
force?: number;
29+
force?: number | undefined;
3030
};
3131

3232
export type PressableEvent = { nativeEvent: InnerPressableEvent };
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export type ExtraButtonProps = {
2-
borderless?: boolean;
3-
rippleColor?: number | string | null;
4-
rippleRadius?: number | null;
5-
foreground?: boolean;
6-
exclusive?: boolean;
2+
borderless?: boolean | undefined;
3+
rippleColor?: number | string | null | undefined;
4+
rippleRadius?: number | null | undefined;
5+
foreground?: boolean | undefined;
6+
exclusive?: boolean | undefined;
77
};

packages/react-native-gesture-handler/src/components/touchables/TouchableHighlight.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ import {
1212

1313
interface State {
1414
extraChildStyle: null | {
15-
opacity?: number;
15+
opacity?: number | undefined;
1616
};
1717
extraUnderlayStyle: null | {
18-
backgroundColor?: ColorValue;
18+
backgroundColor?: ColorValue | undefined;
1919
};
2020
}
2121

packages/react-native-gesture-handler/src/handlers/GestureHandlerEventPayload.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export type PanGestureHandlerEventPayload = {
129129
/**
130130
* Object containing additional stylus data.
131131
*/
132-
stylusData?: StylusData;
132+
stylusData?: StylusData | undefined;
133133
};
134134

135135
export type PinchGestureHandlerEventPayload = {
@@ -225,5 +225,5 @@ export type HoverGestureHandlerEventPayload = {
225225
/**
226226
* Object containing additional stylus data.
227227
*/
228-
stylusData?: StylusData;
228+
stylusData?: StylusData | undefined;
229229
};

packages/react-native-gesture-handler/src/handlers/NativeViewGestureHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ export interface NativeViewGestureConfig {
1717
* Determines whether the handler should check for an existing touch event on
1818
* instantiation.
1919
*/
20-
shouldActivateOnStart?: boolean;
20+
shouldActivateOnStart?: boolean | undefined;
2121

2222
/**
2323
* When `true`, cancels all other gesture handlers when this
2424
* `NativeViewGestureHandler` receives an `ACTIVE` state event.
2525
*/
26-
disallowInterruption?: boolean;
26+
disallowInterruption?: boolean | undefined;
2727
}
2828

2929
/**

packages/react-native-gesture-handler/src/handlers/gestureHandlerCommon.ts

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export type HitSlop =
6363
| Partial<
6464
Record<
6565
'left' | 'right' | 'top' | 'bottom' | 'vertical' | 'horizontal',
66-
number
66+
number | undefined
6767
>
6868
>
6969
| Record<'width' | 'left', number>
@@ -181,39 +181,41 @@ export type GestureStateChangeEvent<
181181
> = HandlerStateChangeEventPayload & GestureStateChangeEventPayloadT;
182182

183183
export type CommonGestureConfig = {
184-
enabled?: boolean;
185-
shouldCancelWhenOutside?: boolean;
186-
hitSlop?: HitSlop;
187-
userSelect?: UserSelect;
188-
activeCursor?: ActiveCursor;
189-
mouseButton?: MouseButton;
190-
enableContextMenu?: boolean;
191-
touchAction?: TouchAction;
184+
enabled?: boolean | undefined;
185+
shouldCancelWhenOutside?: boolean | undefined;
186+
hitSlop?: HitSlop | undefined;
187+
userSelect?: UserSelect | undefined;
188+
activeCursor?: ActiveCursor | undefined;
189+
mouseButton?: MouseButton | undefined;
190+
enableContextMenu?: boolean | undefined;
191+
touchAction?: TouchAction | undefined;
192192
};
193193

194194
// Events payloads are types instead of interfaces due to TS limitation.
195195
// See https://github.com/microsoft/TypeScript/issues/15300 for more info.
196196
export type BaseGestureHandlerProps<
197197
ExtraEventPayloadT extends Record<string, unknown> = Record<string, unknown>,
198198
> = CommonGestureConfig & {
199-
id?: string;
200-
waitFor?: React.Ref<unknown> | React.Ref<unknown>[];
201-
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
202-
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
203-
testID?: string;
204-
cancelsTouchesInView?: boolean;
199+
id?: string | undefined;
200+
waitFor?: React.Ref<unknown> | React.Ref<unknown>[] | undefined;
201+
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[] | undefined;
202+
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[] | undefined;
203+
testID?: string | undefined;
204+
cancelsTouchesInView?: boolean | undefined;
205205
// TODO(TS) - fix event types
206-
onBegan?: (event: HandlerStateChangeEvent) => void;
207-
onFailed?: (event: HandlerStateChangeEvent) => void;
208-
onCancelled?: (event: HandlerStateChangeEvent) => void;
209-
onActivated?: (event: HandlerStateChangeEvent) => void;
210-
onEnded?: (event: HandlerStateChangeEvent) => void;
206+
onBegan?: ((event: HandlerStateChangeEvent) => void) | undefined;
207+
onFailed?: ((event: HandlerStateChangeEvent) => void) | undefined;
208+
onCancelled?: ((event: HandlerStateChangeEvent) => void) | undefined;
209+
onActivated?: ((event: HandlerStateChangeEvent) => void) | undefined;
210+
onEnded?: ((event: HandlerStateChangeEvent) => void) | undefined;
211211

212212
// TODO(TS) consider using NativeSyntheticEvent
213-
onGestureEvent?: (event: GestureEvent<ExtraEventPayloadT>) => void;
214-
onHandlerStateChange?: (
215-
event: HandlerStateChangeEvent<ExtraEventPayloadT>
216-
) => void;
213+
onGestureEvent?:
214+
| ((event: GestureEvent<ExtraEventPayloadT>) => void)
215+
| undefined;
216+
onHandlerStateChange?:
217+
| ((event: HandlerStateChangeEvent<ExtraEventPayloadT>) => void)
218+
| undefined;
217219
// Implicit `children` prop has been removed in @types/react^18.0.0
218220
children?: React.ReactNode;
219221
};

0 commit comments

Comments
 (0)