Skip to content

Commit 4225660

Browse files
YevheniiKotyrlom-bert
authored andcommitted
Enable exactOptionalPropertyTypes support (#4012)
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). 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. 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 ``` 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 adff581 commit 4225660

15 files changed

Lines changed: 96 additions & 92 deletions

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

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

2727
/**
2828
* Android only.
2929
*
3030
* Defines radius of native ripple animation used since API level 21.
3131
*/
32-
rippleRadius?: number | null;
32+
rippleRadius?: number | null | undefined;
3333

3434
/**
3535
* Android only.
3636
*
3737
* Set this to true if you want the ripple animation to render outside the view bounds.
3838
*/
39-
borderless?: boolean;
39+
borderless?: boolean | undefined;
4040

4141
/**
4242
* Android only.
4343
*
4444
* Defines whether the ripple animation should be drawn on the foreground of the view.
4545
*/
46-
foreground?: boolean;
46+
foreground?: boolean | undefined;
4747

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

5555
/**
5656
* Style object, use it to set additional styles.
@@ -60,67 +60,67 @@ export interface RawButtonProps
6060
/**
6161
* Invoked on mount and layout changes.
6262
*/
63-
onLayout?: (event: LayoutChangeEvent) => void;
63+
onLayout?: ((event: LayoutChangeEvent) => void) | undefined;
6464

6565
/**
6666
* Used for testing-library compatibility, not passed to the native component.
6767
* @deprecated test-only props are deprecated and will be removed in the future.
6868
*/
6969
// eslint-disable-next-line @typescript-eslint/ban-types
70-
testOnly_onPress?: Function | null;
70+
testOnly_onPress?: Function | null | undefined;
7171

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

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

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

9797
export interface BaseButtonProps extends RawButtonProps {
9898
/**
9999
* Called when the button gets pressed (analogous to `onPress` in
100100
* `TouchableHighlight` from RN core).
101101
*/
102-
onPress?: (pointerInside: boolean) => void;
102+
onPress?: ((pointerInside: boolean) => void) | undefined;
103103

104104
/**
105105
* Called when the button gets pressed and is held for `delayLongPress`
106106
* milliseconds.
107107
*/
108-
onLongPress?: () => void;
108+
onLongPress?: (() => void) | undefined;
109109

110110
/**
111111
* Called when button changes from inactive to active and vice versa. It
112112
* passes active state as a boolean variable as a first parameter for that
113113
* method.
114114
*/
115-
onActiveStateChange?: (active: boolean) => void;
115+
onActiveStateChange?: ((active: boolean) => void) | undefined;
116116
style?: StyleProp<ViewStyle>;
117-
testID?: string;
117+
testID?: string | undefined;
118118

119119
/**
120120
* Delay, in milliseconds, after which the `onLongPress` callback gets called.
121121
* Defaults to 600.
122122
*/
123-
delayLongPress?: number;
123+
delayLongPress?: number | undefined;
124124
}
125125
export interface BaseButtonWithRefProps
126126
extends BaseButtonProps,
@@ -130,14 +130,14 @@ export interface RectButtonProps extends BaseButtonProps {
130130
/**
131131
* Background color that will be dimmed when button is in active state.
132132
*/
133-
underlayColor?: string;
133+
underlayColor?: string | undefined;
134134

135135
/**
136136
* iOS only.
137137
*
138138
* Opacity applied to the underlay when button is in active state.
139139
*/
140-
activeOpacity?: number;
140+
activeOpacity?: number | undefined;
141141
}
142142
export interface RectButtonWithRefProps
143143
extends RectButtonProps,
@@ -149,7 +149,7 @@ export interface BorderlessButtonProps extends BaseButtonProps {
149149
*
150150
* Opacity applied to the button when it is in an active state.
151151
*/
152-
activeOpacity?: number;
152+
activeOpacity?: number | undefined;
153153
}
154154
export interface BorderlessButtonWithRefProps
155155
extends BorderlessButtonProps,

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

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

3131
export type PressableEvent = { nativeEvent: InnerPressableEvent };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export interface DrawerLayoutProps {
216216
* Sets whether the text inside both the drawer and the context window can be selected.
217217
* Values: 'none' | 'text' | 'auto'
218218
*/
219-
userSelect?: UserSelect;
219+
userSelect?: UserSelect | undefined;
220220

221221
/**
222222
* @default 'auto'

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ type SwipeableState = {
217217
dragX: Animated.Value;
218218
rowTranslation: Animated.Value;
219219
rowState: number;
220-
leftWidth?: number;
221-
rightOffset?: number;
222-
rowWidth?: number;
220+
leftWidth?: number | undefined;
221+
rightOffset?: number | undefined;
222+
rowWidth?: number | undefined;
223223
};
224224

225225
/**
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/PanGestureHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ interface CommonPanProperties {
5050
* enableTrackpadTwoFingerGesture swiping with two fingers will also trigger
5151
* the gesture.
5252
*/
53-
enableTrackpadTwoFingerGesture?: boolean;
53+
enableTrackpadTwoFingerGesture?: boolean | undefined;
5454

5555
/**
5656
* A number of fingers that is required to be placed before handler can

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>
@@ -173,39 +173,41 @@ export type GestureStateChangeEvent<
173173
> = HandlerStateChangeEventPayload & GestureStateChangeEventPayloadT;
174174

175175
export type CommonGestureConfig = {
176-
enabled?: boolean;
177-
shouldCancelWhenOutside?: boolean;
178-
hitSlop?: HitSlop;
179-
userSelect?: UserSelect;
180-
activeCursor?: ActiveCursor;
181-
mouseButton?: MouseButton;
182-
enableContextMenu?: boolean;
183-
touchAction?: TouchAction;
176+
enabled?: boolean | undefined;
177+
shouldCancelWhenOutside?: boolean | undefined;
178+
hitSlop?: HitSlop | undefined;
179+
userSelect?: UserSelect | undefined;
180+
activeCursor?: ActiveCursor | undefined;
181+
mouseButton?: MouseButton | undefined;
182+
enableContextMenu?: boolean | undefined;
183+
touchAction?: TouchAction | undefined;
184184
};
185185

186186
// Events payloads are types instead of interfaces due to TS limitation.
187187
// See https://github.com/microsoft/TypeScript/issues/15300 for more info.
188188
export type BaseGestureHandlerProps<
189189
ExtraEventPayloadT extends Record<string, unknown> = Record<string, unknown>,
190190
> = CommonGestureConfig & {
191-
id?: string;
192-
waitFor?: React.Ref<unknown> | React.Ref<unknown>[];
193-
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
194-
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
195-
testID?: string;
196-
cancelsTouchesInView?: boolean;
191+
id?: string | undefined;
192+
waitFor?: React.Ref<unknown> | React.Ref<unknown>[] | undefined;
193+
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[] | undefined;
194+
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[] | undefined;
195+
testID?: string | undefined;
196+
cancelsTouchesInView?: boolean | undefined;
197197
// TODO(TS) - fix event types
198-
onBegan?: (event: HandlerStateChangeEvent) => void;
199-
onFailed?: (event: HandlerStateChangeEvent) => void;
200-
onCancelled?: (event: HandlerStateChangeEvent) => void;
201-
onActivated?: (event: HandlerStateChangeEvent) => void;
202-
onEnded?: (event: HandlerStateChangeEvent) => void;
198+
onBegan?: ((event: HandlerStateChangeEvent) => void) | undefined;
199+
onFailed?: ((event: HandlerStateChangeEvent) => void) | undefined;
200+
onCancelled?: ((event: HandlerStateChangeEvent) => void) | undefined;
201+
onActivated?: ((event: HandlerStateChangeEvent) => void) | undefined;
202+
onEnded?: ((event: HandlerStateChangeEvent) => void) | undefined;
203203

204204
// TODO(TS) consider using NativeSyntheticEvent
205-
onGestureEvent?: (event: GestureEvent<ExtraEventPayloadT>) => void;
206-
onHandlerStateChange?: (
207-
event: HandlerStateChangeEvent<ExtraEventPayloadT>
208-
) => void;
205+
onGestureEvent?:
206+
| ((event: GestureEvent<ExtraEventPayloadT>) => void)
207+
| undefined;
208+
onHandlerStateChange?:
209+
| ((event: HandlerStateChangeEvent<ExtraEventPayloadT>) => void)
210+
| undefined;
209211
// Implicit `children` prop has been removed in @types/react^18.0.0
210212
children?: React.ReactNode;
211213
};

0 commit comments

Comments
 (0)