From 63d3ea17a717f032c59f0de315f4b197ededcfec Mon Sep 17 00:00:00 2001 From: Spencer Ahrens Date: Thu, 16 Feb 2017 18:59:55 -0800 Subject: [PATCH] Improve flow typing Summary: - Properly inherit flow types from base components, including `defaultProps` - template-ify the `Item` type as it flows from the `data` prop into `ItemComponent` Note that for `SectionList` is is harder to do the `Item` typing because each section in the `sections` array can have a different `Item` type, plus all the optional overrides...not sure how to tackle that. Reviewed By: yungsters Differential Revision: D4557523 fbshipit-source-id: a0c5279fcdaabe6aab5fe11743a99c3715a44032 --- .flowconfig | 1 + Examples/UIExplorer/js/FlatListExample.js | 5 +- Libraries/Experimental/FlatList.js | 54 +++++++---- Libraries/Experimental/MetroListView.js | 4 +- Libraries/Experimental/SectionList.js | 39 +++++--- Libraries/Experimental/VirtualizedList.js | 25 +++-- .../Experimental/VirtualizedSectionList.js | 97 +++++++++++++------ .../__flowtests__/FlatList-flowtest.js | 77 +++++++++++++++ .../__flowtests__/SectionList-flowtest.js | 95 ++++++++++++++++++ 9 files changed, 319 insertions(+), 78 deletions(-) create mode 100644 Libraries/Experimental/__flowtests__/FlatList-flowtest.js create mode 100644 Libraries/Experimental/__flowtests__/SectionList-flowtest.js diff --git a/.flowconfig b/.flowconfig index 7f066b595..be21a435e 100644 --- a/.flowconfig +++ b/.flowconfig @@ -44,6 +44,7 @@ suppress_type=$FixMe suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-9]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\) suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-9]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native_oss[a-z,_]*\\)?)\\)?:? #[0-9]+ suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy +suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError unsafe.enable_getters_and_setters=true diff --git a/Examples/UIExplorer/js/FlatListExample.js b/Examples/UIExplorer/js/FlatListExample.js index 0a2b97215..73ebf2488 100644 --- a/Examples/UIExplorer/js/FlatListExample.js +++ b/Examples/UIExplorer/js/FlatListExample.js @@ -108,7 +108,7 @@ class FlatListExample extends React.PureComponent { key={(this.state.horizontal ? 'h' : 'v') + (this.state.fixedHeight ? 'f' : 'd')} legacyImplementation={false} numColumns={1} - onRefresh={() => alert('onRefresh: nothing to refresh :P')} + onRefresh={this._onRefresh} onViewableItemsChanged={this._onViewableItemsChanged} ref={this._captureRef} refreshing={false} @@ -121,6 +121,7 @@ class FlatListExample extends React.PureComponent { _getItemLayout = (data: any, index: number) => { return getItemLayout(data, index, this.state.horizontal); }; + _onRefresh = () => alert('onRefresh: nothing to refresh :P'); _renderItemComponent = ({item}) => { return ( { pressItem(this, key); }; - _listRef: FlatList; + _listRef: FlatList<*>; } diff --git a/Libraries/Experimental/FlatList.js b/Libraries/Experimental/FlatList.js index 92f43cf5b..97075fcdb 100644 --- a/Libraries/Experimental/FlatList.js +++ b/Libraries/Experimental/FlatList.js @@ -41,22 +41,23 @@ const invariant = require('invariant'); import type {StyleObj} from 'StyleSheetTypes'; import type {Viewable} from 'ViewabilityHelper'; +import type {Props as VirtualizedListProps} from 'VirtualizedList'; type Item = any; -type RequiredProps = { +type RequiredProps = { /** * Note this can be a normal class component, or a functional component, such as a render method * on your main component. */ - ItemComponent: ReactClass<{item: Item, index: number}>, + ItemComponent: ReactClass<{item: ItemT, index: number}>, /** * For simplicity, data is just a plain array. If you want to use something else, like an * immutable list, use the underlying `VirtualizedList` directly. */ - data: ?Array, + data: ?Array, }; -type OptionalProps = { +type OptionalProps = { /** * Rendered at the bottom of all the items. */ @@ -79,7 +80,7 @@ type OptionalProps = { * Remember to include separator length (height or width) in your offset calculation if you * specify `SeparatorComponent`. */ - getItemLayout?: (data: ?Array, index: number) => + getItemLayout?: (data: ?Array, index: number) => {length: number, offset: number, index: number}, /** * If true, renders items next to each other horizontally instead of stacked vertically. @@ -90,7 +91,7 @@ type OptionalProps = { * and as the react key to track item re-ordering. The default extractor checks item.key, then * falls back to using the index, like react does. */ - keyExtractor: (item: Item, index: number) => string, + keyExtractor: (item: ItemT, index: number) => string, /** * Multiple columns can only be rendered with horizontal={false} and will zig-zag like a flexWrap * layout. Items should all be the same height - masonry layouts are not supported. @@ -111,6 +112,7 @@ type OptionalProps = { * `viewablePercentThreshold` prop. */ onViewableItemsChanged?: ?({viewableItems: Array, changed: Array}) => void, + legacyImplementation?: ?boolean, /** * Set this true while waiting for new data from a refresh. */ @@ -123,11 +125,19 @@ type OptionalProps = { * Optional optimization to minimize re-rendering items. */ shouldItemUpdate: ( - prevProps: {item: Item, index: number}, - nextProps: {item: Item, index: number} + prevProps: {item: ItemT, index: number}, + nextProps: {item: ItemT, index: number} ) => boolean, }; -type Props = RequiredProps & OptionalProps; // plus props from the underlying implementation +type Props = RequiredProps & OptionalProps & VirtualizedListProps; + +const defaultProps = { + ...VirtualizedList.defaultProps, + getItem: undefined, + getItemCount: undefined, + numColumns: 1, +}; +type DefaultProps = typeof defaultProps; /** * A performant interface for rendering simple, flat lists, supporting the most handy features: @@ -148,13 +158,9 @@ type Props = RequiredProps & OptionalProps; // plus props from the underlying im * ItemComponent={({item}) => {item.key}} * /> */ -class FlatList extends React.PureComponent { - static defaultProps = { - keyExtractor: VirtualizedList.defaultProps.keyExtractor, - numColumns: 1, - shouldItemUpdate: VirtualizedList.defaultProps.shouldItemUpdate, - }; - props: Props; +class FlatList extends React.PureComponent, void> { + static defaultProps: DefaultProps = defaultProps; + props: Props; /** * Scrolls to the end of the content. May be janky without getItemLayout prop. */ @@ -191,7 +197,7 @@ class FlatList extends React.PureComponent { this._checkProps(this.props); } - componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps: Props) { this._checkProps(nextProps); } @@ -200,7 +206,7 @@ class FlatList extends React.PureComponent { _captureRef = (ref) => { this._listRef = ref; }; - _checkProps(props: Props) { + _checkProps(props: Props) { const { getItem, getItemCount, @@ -229,7 +235,7 @@ class FlatList extends React.PureComponent { } } - _getItem = (data: Array, index: number): Item | Array => { + _getItem = (data: Array, index: number): ItemT | Array => { const {numColumns} = this.props; if (numColumns > 1) { const ret = []; @@ -243,13 +249,19 @@ class FlatList extends React.PureComponent { } }; - _getItemCount = (data: Array): number => { + _getItemCount = (data: Array): number => { return Math.floor(data.length / this.props.numColumns); }; - _keyExtractor = (items: Item | Array, index: number): string => { + _keyExtractor = (items: ItemT | Array, index: number): string => { const {keyExtractor, numColumns} = this.props; if (numColumns > 1) { + invariant( + Array.isArray(items), + 'FlatList: Encountered internal consistency error, expected each item to consist of an ' + + 'array with 1-%s columns; instead, received a single item.', + numColumns, + ); return items.map((it, kk) => keyExtractor(it, index * numColumns + kk)).join(':'); } else { return keyExtractor(items, index); diff --git a/Libraries/Experimental/MetroListView.js b/Libraries/Experimental/MetroListView.js index 7533bb166..b5d12742f 100644 --- a/Libraries/Experimental/MetroListView.js +++ b/Libraries/Experimental/MetroListView.js @@ -49,7 +49,7 @@ type NormalProps = { // Provide either `items` or `sections` items?: ?Array, // By default, an Item is assumed to be {key: string} - sections?: ?Array<{key: string, items: Array}>, + sections?: ?Array<{key: string, data: Array}>, /** * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make @@ -146,7 +146,7 @@ class MetroListView extends React.Component { const sections = {}; props.sections.forEach((sectionIn, ii) => { const sectionID = 's' + ii; - sections[sectionID] = sectionIn.itemData; + sections[sectionID] = sectionIn.data; sectionHeaderData[sectionID] = sectionIn; }); return { diff --git a/Libraries/Experimental/SectionList.js b/Libraries/Experimental/SectionList.js index d756be589..c7dfca6de 100644 --- a/Libraries/Experimental/SectionList.js +++ b/Libraries/Experimental/SectionList.js @@ -37,19 +37,19 @@ const React = require('React'); const VirtualizedSectionList = require('VirtualizedSectionList'); import type {Viewable} from 'ViewabilityHelper'; +import type {Props as VirtualizedSectionListProps} from 'VirtualizedSectionList'; type Item = any; -type SectionItem = any; -type SectionBase = { +type SectionBase = { // Must be provided directly on each section. - data: Array, + data: Array, key: string, // Optional props will override list-wide props just for this section. - ItemComponent?: ?ReactClass<{item: SectionItem, index: number}>, + ItemComponent?: ?ReactClass<{item: SectionItemT, index: number}>, SeparatorComponent?: ?ReactClass<*>, - keyExtractor?: (item: SectionItem) => string, + keyExtractor?: (item: SectionItemT) => string, // TODO: support more optional/override props // FooterComponent?: ?ReactClass<*>, @@ -57,15 +57,15 @@ type SectionBase = { // onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, // TODO: support recursive sections - // SectionHeaderComponent?: ?ReactClass<{section: SectionBase}>, + // SectionHeaderComponent?: ?ReactClass<{section: SectionBase<*>}>, // sections?: ?Array
; }; -type RequiredProps = { +type RequiredProps> = { sections: Array, }; -type OptionalProps = { +type OptionalProps> = { /** * Rendered after the last item in the last section. */ @@ -73,7 +73,7 @@ type OptionalProps = { /** * Default renderer for every item in every section. */ - ItemComponent?: ?ReactClass<{item: Item, index: number}>, + ItemComponent: ReactClass<{item: Item, index: number}>, /** * Rendered at the top of each section. In the future, a sticky option will be added. */ @@ -93,8 +93,8 @@ type OptionalProps = { * stored outside of the recursive `ItemComponent` instance tree. */ enableVirtualization?: ?boolean, - keyExtractor?: (item: Item) => string, - onEndReached?: ({distanceFromEnd: number}) => void, + keyExtractor: (item: Item, index: number) => string, + onEndReached?: ?({distanceFromEnd: number}) => void, /** * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make * sure to also set the `refreshing` prop correctly. @@ -104,21 +104,25 @@ type OptionalProps = { * Called when the viewability of rows changes, as defined by the * `viewablePercentThreshold` prop. */ - onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, + onViewableItemsChanged?: ?({viewableItems: Array, changed: Array}) => void, /** * Set this true while waiting for new data from a refresh. */ - refreshing?: boolean, + refreshing?: ?boolean, /** * This is an optional optimization to minimize re-rendering items. */ - shouldItemUpdate?: ( + shouldItemUpdate: ( prevProps: {item: Item, index: number}, nextProps: {item: Item, index: number} ) => boolean, }; -type Props = RequiredProps & OptionalProps; +type Props = RequiredProps + & OptionalProps + & VirtualizedSectionListProps; + +type DefaultProps = typeof VirtualizedSectionList.defaultProps; /** * A performant interface for rendering sectioned lists, supporting the most handy features: @@ -132,8 +136,11 @@ type Props = RequiredProps & OptionalProps; * * If you don't need section support and want a simpler interface, use FlatList. */ -class SectionList extends React.Component, void> { +class SectionList> + extends React.PureComponent, *> +{ props: Props; + static defaultProps: DefaultProps = VirtualizedSectionList.defaultProps; render() { if (this.props.legacyImplementation) { diff --git a/Libraries/Experimental/VirtualizedList.js b/Libraries/Experimental/VirtualizedList.js index 8d90c286e..b91524814 100644 --- a/Libraries/Experimental/VirtualizedList.js +++ b/Libraries/Experimental/VirtualizedList.js @@ -68,7 +68,7 @@ type RequiredProps = { * The default accessor functions assume this is an Array<{key: string}> but you can override * getItem, getItemCount, and keyExtractor to handle any type of index-based data. */ - data: any, + data?: any, }; type OptionalProps = { FooterComponent?: ?ReactClass<*>, @@ -89,12 +89,12 @@ type OptionalProps = { getItemCount: (items: any) => number, getItemLayout?: (items: any, index: number) => {length: number, offset: number, index: number}, // e.g. height, y - horizontal: boolean, + horizontal?: ?boolean, initialNumToRender: number, keyExtractor: (item: Item, index: number) => string, maxToRenderPerBatch: number, - onEndReached: ({distanceFromEnd: number}) => void, - onEndReachedThreshold: number, // units of visible length + onEndReached?: ?({distanceFromEnd: number}) => void, + onEndReachedThreshold?: ?number, // units of visible length onLayout?: ?Function, /** * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make @@ -105,11 +105,11 @@ type OptionalProps = { * Called when the viewability of rows changes, as defined by the * `viewablePercentThreshold` prop. */ - onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, + onViewableItemsChanged?: ?({viewableItems: Array, changed: Array}) => void, /** * Set this true while waiting for new data from a refresh. */ - refreshing?: boolean, + refreshing?: ?boolean, removeClippedSubviews?: boolean, renderScrollComponent: (props: Object) => React.Element<*>, shouldItemUpdate: ( @@ -126,11 +126,11 @@ type OptionalProps = { viewablePercentThreshold: number, windowSize: number, // units of visible length }; -type Props = RequiredProps & OptionalProps; +export type Props = RequiredProps & OptionalProps; let _usedIndexForKey = false; -class VirtualizedList extends React.PureComponent { +class VirtualizedList extends React.PureComponent { props: Props; // scrollToEnd may be janky without getItemLayout prop @@ -182,7 +182,7 @@ class VirtualizedList extends React.PureComponent { ); } - static defaultProps: OptionalProps = { + static defaultProps = { disableVirtualization: false, getItem: (data: any, index: number) => data[index], getItemCount: (data: any) => data ? data.length : 0, @@ -390,7 +390,12 @@ class VirtualizedList extends React.PureComponent { _onCellLayout = (e, cellKey, index) => { const layout = e.nativeEvent.layout; - const next = {offset: this._selectOffset(layout), length: this._selectLength(layout), index, inLayout: true}; + const next = { + offset: this._selectOffset(layout), + length: this._selectLength(layout), + index, + inLayout: true, + }; const curr = this._frames[cellKey]; if (!curr || next.offset !== curr.offset || diff --git a/Libraries/Experimental/VirtualizedSectionList.js b/Libraries/Experimental/VirtualizedSectionList.js index c86cf2b34..620c4f343 100644 --- a/Libraries/Experimental/VirtualizedSectionList.js +++ b/Libraries/Experimental/VirtualizedSectionList.js @@ -40,11 +40,12 @@ const invariant = require('invariant'); const warning = require('warning'); import type {Viewable} from 'ViewabilityHelper'; +import type {Props as VirtualizedListProps} from 'VirtualizedList'; type Item = any; type SectionItem = any; -type Section = { +type SectionBase = { // Must be provided directly on each section. data: Array, key: string, @@ -60,17 +61,35 @@ type Section = { // onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, // TODO: support recursive sections - // SectionHeaderComponent?: ?ReactClass<{section: Section}>, + // SectionHeaderComponent?: ?ReactClass<{section: SectionBase}>, // sections?: ?Array
; -} - -type RequiredProps = { - sections: Array
, }; -type OptionalProps = { - ItemComponent?: ?ReactClass<{item: Item, index: number}>, - SectionHeaderComponent?: ?ReactClass<{section: Section}>, + +type RequiredProps = { + sections: Array, +}; + +type OptionalProps = { + /** + * Rendered after the last item in the last section. + */ + FooterComponent?: ?ReactClass<*>, + /** + * Default renderer for every item in every section. + */ + ItemComponent: ReactClass<{item: Item, index: number}>, + /** + * Rendered at the top of each section. In the future, a sticky option will be added. + */ + SectionHeaderComponent?: ?ReactClass<{section: SectionT}>, + /** + * Rendered at the bottom of every Section, except the very last one, in place of the normal + * SeparatorComponent. + */ SectionSeparatorComponent?: ?ReactClass<*>, + /** + * Rendered at the bottom of every Item except the very last one in the last section. + */ SeparatorComponent?: ?ReactClass<*>, /** * Warning: Virtualization can drastically improve memory consumption for long lists, but trashes @@ -78,42 +97,65 @@ type OptionalProps = { * stored outside of the recursive `ItemComponent` instance tree. */ enableVirtualization?: ?boolean, - horizontal?: ?boolean, - keyExtractor?: (item: Item, index: number) => string, - onEndReached?: ({distanceFromEnd: number}) => void, + keyExtractor: (item: Item, index: number) => string, + onEndReached?: ?({distanceFromEnd: number}) => void, + /** + * If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make + * sure to also set the `refreshing` prop correctly. + */ + onRefresh?: ?Function, /** * Called when the viewability of rows changes, as defined by the - * `viewablePercentThreshold` prop. Called for all items from all sections. + * `viewablePercentThreshold` prop. */ - onViewableItemsChanged?: ({viewableItems: Array, changed: Array}) => void, + onViewableItemsChanged?: ?({viewableItems: Array, changed: Array}) => void, + /** + * Set this true while waiting for new data from a refresh. + */ + refreshing?: ?boolean, + /** + * This is an optional optimization to minimize re-rendering items. + */ + shouldItemUpdate: ( + prevProps: {item: Item, index: number}, + nextProps: {item: Item, index: number} + ) => boolean, }; -type Props = RequiredProps & OptionalProps; + +export type Props = + RequiredProps & + OptionalProps & + VirtualizedListProps; + +type DefaultProps = (typeof VirtualizedList.defaultProps) & {data: Array}; +type State = {childProps: VirtualizedListProps}; /** * Right now this just flattens everything into one list and uses VirtualizedList under the * hood. The only operation that might not scale well is concatting the data arrays of all the * sections when new props are received, which should be plenty fast for up to ~10,000 items. */ -class VirtualizedSectionList extends React.PureComponent { - props: Props; +class VirtualizedSectionList + extends React.PureComponent, State> +{ + props: Props; - state: { - childProps: Object, - }; + state: State; - static defaultProps: OptionalProps = { - keyExtractor: (item: Item, index: number) => item.key || String(index), + static defaultProps: DefaultProps = { + ...VirtualizedList.defaultProps, + data: [], }; _keyExtractor = (item: Item, index: number) => { const info = this._subExtractor(index); - return info && info.key; + return (info && info.key) || String(index); }; _subExtractor( index: number, ): ?{ - section: Section, + section: SectionT, key: string, // Key of the section or combined key for section + item index: ?number, // Relative index within the section } { @@ -221,9 +263,10 @@ class VirtualizedSectionList extends React.PureComponent { return true; } } + return false; } - _computeState(props: Props) { + _computeState(props: Props): State { const itemCount = props.sections.reduce((v, section) => v + section.data.length + 1, 0); return { childProps: { @@ -242,7 +285,7 @@ class VirtualizedSectionList extends React.PureComponent { }; } - constructor(props: Props, context: Object) { + constructor(props: Props, context: Object) { super(props, context); warning( !props.stickySectionHeadersEnabled, @@ -251,7 +294,7 @@ class VirtualizedSectionList extends React.PureComponent { this.state = this._computeState(props); } - componentWillReceiveProps(nextProps: Props) { + componentWillReceiveProps(nextProps: Props) { this.setState(this._computeState(nextProps)); } diff --git a/Libraries/Experimental/__flowtests__/FlatList-flowtest.js b/Libraries/Experimental/__flowtests__/FlatList-flowtest.js new file mode 100644 index 000000000..83d90947b --- /dev/null +++ b/Libraries/Experimental/__flowtests__/FlatList-flowtest.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +'use strict'; + +const FlatList = require('FlatList'); +const React = require('react'); + +class MyListItem extends React.Component { + props: { + item: { + title: string, + }, + }; + render() { + return ; + } +} + +module.exports = { + testBadDataWithTypicalItemComponent(): React.Element<*> { + // $FlowExpectedError - bad title type 6, should be string + const data = [{ + title: 6, + key: 1, + }]; + return ; + }, + + testMissingFieldWithTypicalItemComponent(): React.Element<*> { + const data = [{ + key: 1, + }]; + // $FlowExpectedError - missing title + return ; + }, + + testGoodDataWithGoodCustomItemComponentFunction() { + const data = [{ + widgetCount: 3, + key: 1, + }]; + return ( + => + + } + data={data} + /> + ); + }, + + testBadNonInheritedDefaultProp(): React.Element<*> { + const data = []; + // $FlowExpectedError - bad numColumns type "lots" + return ; + }, + + testBadInheritedDefaultProp(): React.Element<*> { + const data = []; + // $FlowExpectedError - bad windowSize type "big" + return ; + }, + + testMissingData(): React.Element<*> { + // $FlowExpectedError - missing `data` prop + return ; + }, +}; diff --git a/Libraries/Experimental/__flowtests__/SectionList-flowtest.js b/Libraries/Experimental/__flowtests__/SectionList-flowtest.js new file mode 100644 index 000000000..fda0e5c45 --- /dev/null +++ b/Libraries/Experimental/__flowtests__/SectionList-flowtest.js @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +'use strict'; + +const React = require('react'); +const SectionList = require('SectionList'); + +class MyListItem extends React.Component { + props: { + item: { + title: string, + }, + }; + render() { + return ; + } +} + +class MyHeader extends React.Component { + props: { + section: { + fooNumber: number, + } + }; + render() { + return ; + } +} + +module.exports = { + testGoodDataWithGoodCustomItemComponentFunction() { + const sections = [{ + key: 'a', data: [{ + widgetCount: 3, + key: 1, + }], + }]; + return ( + => + + } + sections={sections} + /> + ); + }, + + testBadInheritedDefaultProp(): React.Element<*> { + const sections = []; + // $FlowExpectedError - bad windowSize type "big" + return ; + }, + + testMissingData(): React.Element<*> { + // $FlowExpectedError - missing `sections` prop + return ; + }, + + testBadSectionsShape(): React.Element<*> { + const sections = [{ + key: 'a', items: [{ + title: 'foo', + key: 1, + }], + }]; + // $FlowExpectedError - section missing `data` field + return ; + }, + + testBadSectionsMetadata(): React.Element<*> { + // $FlowExpectedError - section has bad meta data `fooNumber` field of type string + const sections = [{ + key: 'a', fooNumber: 'string', data: [{ + title: 'foo', + key: 1, + }], + }]; + return ( + + ); + }, +};