Skip to content

Commit 778ae87

Browse files
huntiefacebook-github-bot
authored andcommitted
Fix ref type compatibility for builtin components (#56673)
Summary: Fix `useRef` and `forwardRef` backcompat when migrating to React Native's generated TypeScript types (Strict TypeScript API). Previously this was an awkward breaking change: https://reactnative.dev/docs/strict-typescript-api#some-core-components-are-now-function-components-instead-of-class-components ```diff - const ref = useRef<View>(null); + const ref = useRef<React.ComponentRef<typeof View>>(null); ``` After this diff, no workaround will be required. **Changes** - Add companion type aliases alongside each component's value export. - Introduces a new `/* ts-only */` comment syntax in `index.js.flow` to hide the companion exports from Flow, which has no equivalent concept of declaration merging. **Problem detail** In our Flow source code, built-in components like `View` and `TextInput` are function components rather than classes. This differs from the previous manual `.d.ts` files which [defined each component as a class](https://github.com/facebook/react-native/blob/d1cdce24cca2224f8d37873d24a49523a8271a0a/packages/react-native/Libraries/Text/Text.d.ts#L226). In TypeScript, a class name serves as both a value (the constructor) and a type (the instance), so patterns like `useRef<View>()` work. However, with function components, the name is only a value (the function signature) making `useRef<View>()` resolve to a ref holding the function itself, not the native element. **The fix**: Add companion type aliases alongside each component's value export. TypeScript's declaration merging makes the component name simultaneously a value (for JSX) and a type (for refs). ```diff // Before — required workaround - const ref = useRef<View>(null); // TS2749: 'View' refers to a value, not a type + const ref = useRef<React.ComponentRef<typeof View>>(null); ref.current?.measure(...); - const inputRef = useRef<TextInput>(null); // TS2749: 'TextInput' refers to a value, not a type + const inputRef = useRef<React.ComponentRef<typeof TextInput>>(null); inputRef.current?.focus(); // After — original patterns work as-is const ref = useRef<View>(null); // View as a type = HostInstance ref.current?.measure(...); // HostInstance has measure() const inputRef = useRef<TextInput>(null); // TextInput as a type = TextInputInstance inputRef.current?.focus(); // TextInputInstance has focus() ``` Changelog: [General][Fixed] - Add companion instance type aliases for built-in components, preserving `useRef<Component>` patterns under the Strict TypeScript API Differential Revision: D103601923
1 parent d1cdce2 commit 778ae87

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

packages/react-native/index.js.flow

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,33 @@
2222

2323
// #region Components
2424

25+
// - TypeScript function component interop -
26+
//
27+
// Each component has a companion type export (/* @ts-only */) mapping its name
28+
// to its instance type. This enables ref type compatibility under TypeScript
29+
// (e.g. `useRef<View>()`) where `View` resolves to `ReactNativeElement` rather
30+
// than the component function type.
31+
32+
/* eslint-disable no-unused-vars */
33+
import type {PublicScrollViewInstance as _PublicScrollViewInstance} from './Libraries/Components/ScrollView/ScrollView';
34+
import type {TextInputInstance as _TextInputInstance} from './Libraries/Components/TextInput/TextInput.flow';
35+
import type {HostInstance as _HostInstance} from './src/private/types/HostInstance';
36+
/* eslint-enable no-unused-vars */
37+
2538
export type {ActivityIndicatorProps} from './Libraries/Components/ActivityIndicator/ActivityIndicator';
2639
export {default as ActivityIndicator} from './Libraries/Components/ActivityIndicator/ActivityIndicator';
40+
/* @ts-only export type ActivityIndicator = _HostInstance; */
2741

2842
export type {ButtonProps} from './Libraries/Components/Button';
2943
export {default as Button} from './Libraries/Components/Button';
44+
/* @ts-only export type Button = _HostInstance; */
3045

3146
export type {
3247
DrawerLayoutAndroidProps,
3348
DrawerSlideEvent,
3449
} from './Libraries/Components/DrawerAndroid/DrawerLayoutAndroid';
3550
export {default as DrawerLayoutAndroid} from './Libraries/Components/DrawerAndroid/DrawerLayoutAndroid';
51+
/* @ts-only export type DrawerLayoutAndroid = _HostInstance; */
3652

3753
export type {FlatListProps} from './Libraries/Lists/FlatList';
3854
export {default as FlatList} from './Libraries/Lists/FlatList';
@@ -56,13 +72,17 @@ export type {
5672
ImageURISource,
5773
} from './Libraries/Image/ImageSource';
5874
export {default as Image} from './Libraries/Image/Image';
75+
/* @ts-only export type Image = _HostInstance; */
5976
export {default as ImageBackground} from './Libraries/Image/ImageBackground';
77+
/* @ts-only export type ImageBackground = _HostInstance; */
6078

6179
export type {InputAccessoryViewProps} from './Libraries/Components/TextInput/InputAccessoryView';
6280
export {default as InputAccessoryView} from './Libraries/Components/TextInput/InputAccessoryView';
81+
/* @ts-only export type InputAccessoryView = _HostInstance; */
6382

6483
export type {KeyboardAvoidingViewProps} from './Libraries/Components/Keyboard/KeyboardAvoidingView';
6584
export {default as KeyboardAvoidingView} from './Libraries/Components/Keyboard/KeyboardAvoidingView';
85+
/* @ts-only export type KeyboardAvoidingView = _HostInstance; */
6686

6787
export type {LayoutConformanceProps} from './Libraries/Components/LayoutConformance/LayoutConformance';
6888
export {default as experimental_LayoutConformance} from './Libraries/Components/LayoutConformance/LayoutConformance';
@@ -74,25 +94,30 @@ export type {
7494
ModalPropsIOS,
7595
} from './Libraries/Modal/Modal';
7696
export {default as Modal} from './Libraries/Modal/Modal';
97+
/* @ts-only export type Modal = _HostInstance; */
7798

7899
export type {
79100
PressableAndroidRippleConfig,
80101
PressableProps,
81102
PressableStateCallbackType,
82103
} from './Libraries/Components/Pressable/Pressable';
83104
export {default as Pressable} from './Libraries/Components/Pressable/Pressable';
105+
/* @ts-only export type Pressable = _HostInstance; */
84106

85107
export type {ProgressBarAndroidProps} from './Libraries/Components/ProgressBarAndroid/ProgressBarAndroid';
86108
export {default as ProgressBarAndroid} from './Libraries/Components/ProgressBarAndroid/ProgressBarAndroid';
109+
/* @ts-only export type ProgressBarAndroid = _HostInstance; */
87110

88111
export type {
89112
RefreshControlProps,
90113
RefreshControlPropsAndroid,
91114
RefreshControlPropsIOS,
92115
} from './Libraries/Components/RefreshControl/RefreshControl';
93116
export {default as RefreshControl} from './Libraries/Components/RefreshControl/RefreshControl';
117+
/* @ts-only export type RefreshControl = _HostInstance; */
94118

95119
export {default as SafeAreaView} from './Libraries/Components/SafeAreaView/SafeAreaView';
120+
/* @ts-only export type SafeAreaView = _HostInstance; */
96121

97122
export type {
98123
ScrollViewImperativeMethods,
@@ -103,6 +128,7 @@ export type {
103128
ScrollViewPropsIOS,
104129
} from './Libraries/Components/ScrollView/ScrollView';
105130
export {default as ScrollView} from './Libraries/Components/ScrollView/ScrollView';
131+
/* @ts-only export type ScrollView = _PublicScrollViewInstance; */
106132

107133
export type {
108134
SectionListProps,
@@ -118,15 +144,18 @@ export type {
118144
StatusBarStyle,
119145
} from './Libraries/Components/StatusBar/StatusBar';
120146
export {default as StatusBar} from './Libraries/Components/StatusBar/StatusBar';
147+
/* @ts-only export type StatusBar = _HostInstance; */
121148

122149
export type {
123150
SwitchChangeEvent,
124151
SwitchProps,
125152
} from './Libraries/Components/Switch/Switch';
126153
export {default as Switch} from './Libraries/Components/Switch/Switch';
154+
/* @ts-only export type Switch = _HostInstance; */
127155

128156
export type {TextProps} from './Libraries/Text/Text';
129157
export {default as Text} from './Libraries/Text/Text';
158+
/* @ts-only export type Text = _HostInstance; */
130159
export type {NativeTextProps as unstable_NativeTextProps} from './Libraries/Text/TextNativeComponent';
131160
export {NativeText as unstable_NativeText} from './Libraries/Text/TextNativeComponent';
132161
export {default as unstable_TextAncestorContext} from './Libraries/Text/TextAncestorContext';
@@ -151,20 +180,25 @@ export type {
151180
SubmitBehavior,
152181
} from './Libraries/Components/TextInput/TextInput';
153182
export {default as TextInput} from './Libraries/Components/TextInput/TextInput';
183+
/* @ts-only export type TextInput = _TextInputInstance; */
154184

155185
export {default as Touchable} from './Libraries/Components/Touchable/Touchable';
156186

157187
export type {TouchableHighlightProps} from './Libraries/Components/Touchable/TouchableHighlight';
158188
export {default as TouchableHighlight} from './Libraries/Components/Touchable/TouchableHighlight';
189+
/* @ts-only export type TouchableHighlight = _HostInstance; */
159190

160191
export type {TouchableNativeFeedbackProps} from './Libraries/Components/Touchable/TouchableNativeFeedback';
161192
export {default as TouchableNativeFeedback} from './Libraries/Components/Touchable/TouchableNativeFeedback';
193+
/* @ts-only export type TouchableNativeFeedback = _HostInstance; */
162194

163195
export type {TouchableOpacityProps} from './Libraries/Components/Touchable/TouchableOpacity';
164196
export {default as TouchableOpacity} from './Libraries/Components/Touchable/TouchableOpacity';
197+
/* @ts-only export type TouchableOpacity = _HostInstance; */
165198

166199
export type {TouchableWithoutFeedbackProps} from './Libraries/Components/Touchable/TouchableWithoutFeedback';
167200
export {default as TouchableWithoutFeedback} from './Libraries/Components/Touchable/TouchableWithoutFeedback';
201+
/* @ts-only export type TouchableWithoutFeedback = _HostInstance; */
168202

169203
export type {
170204
AccessibilityActionEvent,
@@ -182,6 +216,7 @@ export type {
182216
ViewPropsIOS,
183217
} from './Libraries/Components/View/ViewPropTypes';
184218
export {default as View} from './Libraries/Components/View/View';
219+
/* @ts-only export type View = _HostInstance; */
185220
export {default as unstable_NativeView} from './Libraries/Components/View/ViewNativeComponent';
186221

187222
export type {

scripts/js-api/build-types/translateSourceFile.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,14 @@ async function translateSourceFile(
5050
source: string,
5151
filePath: string,
5252
): Promise<TranslateSourceFileResult> {
53+
// Uncomment /* @ts-only ... */ blocks
54+
const preprocessed = source.replace(
55+
/\/\* @ts-only\s*([\s\S]*?)\s*\*\//g,
56+
'$1',
57+
);
58+
5359
// Parse Flow source
54-
const parsed = await parse(source);
60+
const parsed = await parse(preprocessed);
5561

5662
// Apply pre-transforms
5763
const preTransformResult = await applyPreTransforms(parsed);

0 commit comments

Comments
 (0)