Last active
April 14, 2023 11:51
-
-
Save lafiosca/d6fe710fd23b66af6221648666e6dffd to your computer and use it in GitHub Desktop.
React Native TypeScript children of ForwardRefExoticComponents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { | |
forwardRef, | |
FunctionComponent, | |
PropsWithChildren, | |
Ref, | |
} from 'react'; | |
import { View, Text, ViewProps } from 'react-native'; | |
/* | |
* First we create a simple function component which forwards a View ref: | |
*/ | |
interface FooProps extends ViewProps { | |
fooText: string; | |
} | |
export const Foo = forwardRef<View, FooProps>(({ fooText, children, ...props }, ref) => ( | |
<View {...props} ref={ref}> | |
<Text>{fooText}</Text> | |
{children} | |
</View> | |
)); | |
/* | |
* Next we'll try wrapping that component in other components: | |
*/ | |
interface BarProps extends FooProps { | |
barText: string; | |
} | |
/* | |
* For Bar1, respreading the props to Foo and adding children works without complaint. | |
* | |
* Foo has type: React.ForwardRefExoticComponent<FooProps & React.RefAttributes<View>> | |
* props has type: { | |
* fooText: string; | |
* hitSlop?: Insets | undefined; | |
* onLayout?: ((event: LayoutChangeEvent) => void) | undefined; | |
* pointerEvents?: "box-none" | "none" | "box-only" | "auto" | undefined; | |
* ... 49 more ...; | |
* children?: React.ReactNode; | |
* } | |
*/ | |
export const Bar1: FunctionComponent<BarProps> = ({ barText, ...props }) => ( | |
<Foo {...props}> | |
<Text>{barText}</Text> | |
</Foo> | |
); | |
/* | |
* For Bar2, TypeScript complains: | |
* | |
* Type '{ children: Element; fooText: string; hitSlop?: Insets | undefined; onLayout?: | |
* ((event: LayoutChangeEvent) => void) | undefined; pointerEvents?: "box-none" | ... 3 more ... | |
* | undefined; ... 48 more ...; accessibilityIgnoresInvertColors?: boolean | undefined; }' | |
* is not assignable to type 'IntrinsicAttributes & FooProps & RefAttributes<View>'. | |
* Property 'children' does not exist on type 'IntrinsicAttributes & FooProps & RefAttributes<View>'. ts(2322) | |
*/ | |
export const Bar2: FunctionComponent<BarProps> = ({ fooText, barText, ...props }) => ( | |
<Foo {...props} fooText={`${fooText} plus:`}> | |
<Text>{barText}</Text> | |
</Foo> | |
); | |
/* | |
* For Bar3, TypeScript complains: | |
* | |
* Type '{ children: Element; fooText: string; }' is not assignable to type 'IntrinsicAttributes | |
* & FooProps & RefAttributes<View>'. | |
* Property 'children' does not exist on type 'IntrinsicAttributes & FooProps & RefAttributes<View>'. ts(2322) | |
*/ | |
export const Bar3: FunctionComponent<BarProps> = ({ barText }) => ( | |
<Foo fooText="constant"> | |
<Text>{barText}</Text> | |
</Foo> | |
); | |
/* | |
* If we do the same thing as above but explicitly specify the type, adding children to Foo's props... | |
*/ | |
export const NewFoo = forwardRef<View, PropsWithChildren<FooProps>>(({ fooText, children, ...props }, ref) => ( | |
<View {...props} ref={ref}> | |
<Text>{fooText}</Text> | |
{children} | |
</View> | |
)); | |
/* ...this works fine: */ | |
export const NewBar3: FunctionComponent<BarProps> = ({ barText }) => ( | |
<NewFoo fooText="constant"> | |
<Text>{barText}</Text> | |
</NewFoo> | |
); | |
/* | |
* But if we allow the same children-augmented props type to be inferred for forwardRef... | |
*/ | |
export const NewBadFoo = forwardRef(({ fooText, children, ...props }: PropsWithChildren<FooProps>, ref: Ref<View>) => ( | |
<View {...props} ref={ref}> | |
<Text>{fooText}</Text> | |
{children} | |
</View> | |
)); | |
/* | |
* ...this does not work: | |
* | |
* const NewBadFoo: React.ForwardRefExoticComponent<FooProps & React.RefAttributes<View>> | |
* Type '{ children: Element; fooText: string; }' is not assignable to type | |
* 'IntrinsicAttributes & FooProps & RefAttributes<View>'. | |
* Property 'children' does not exist on type 'IntrinsicAttributes & FooProps & RefAttributes<View>'. ts(2322) | |
*/ | |
export const NewBadBar3: FunctionComponent<BarProps> = ({ barText }) => ( | |
<NewBadFoo fooText="constant"> | |
<Text>{barText}</Text> | |
</NewBadFoo> | |
); | |
/* | |
* Summary: why does Bar1 work when Bar2 and Bar3 do not? | |
* | |
* - Should Bar2 and Bar3 work? | |
* | |
* - If not, and Bar1 should not work, is this just a peculiarity of TypeScript? | |
* | |
* - Is it a little odd that the props argument of the component function argument | |
* passed to forwardRef automatically includes `children?` in its typing but the | |
* resulting forwarded ref component's props argument does not? | |
* | |
* - Is it odd that in the case of NewBadFoo, forwardRef strips the `children?` | |
* from the props that it infers? | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment