From 9039892f8ea3823b0a37d2b9764ba413a9eb6aa9 Mon Sep 17 00:00:00 2001 From: Jessica Franco Date: Fri, 9 Nov 2018 15:35:23 +0900 Subject: [PATCH] Add more of the new features added in React 16.6 (#30054) * Add definitions for React.memo * Add missing semicolons * Give a better name to the second argument to React.memo * Fix test to reflect correct usage of React.memo's second argument * Fix no-unnecessary-qualifier lints * Update other special components to be SpecialSFC * Ensure ordinary functions aren't assignable to SpecialSFC * createElement was resolving P to "{} | null" in some tests; add extends to prevent that * Fix the props of Fragment and StrictMode * Add tests for SpecialSFC assignability * Add scary doc comment to SpecialSFC's call signature Hopefully this trips tslint's deprecation rule. * Rename SpecialSFC to ExoticComponent * Add overload to React.memo to make it more ergonomic and avoid implicit any * Disable test that requires TS 3.1 * Add support for displayName in the exotic components that support it * Tone down the call signature's doc comment * Correct $ExpectType assertion * Try to implement LibraryManagedAttributes for the ExoticComponents that should support them This doesn't actually work because it seems only class components get them checked? * Add type definitions for new React 16.6 features This also attempts to add support for LibraryManagedAttributes to the ExoticComponents. * The fallback prop is required by Suspense * Declare legacy context on tests that use legacy context --- .../react-virtualized-tests.tsx | 9 +- types/react/index.d.ts | 162 +++++++++++++++--- types/react/test/index.ts | 15 ++ types/react/test/managedAttributes.tsx | 80 ++++++++- types/react/test/tsx.tsx | 57 ++++++ types/theming/theming-tests.tsx | 1 + 6 files changed, 294 insertions(+), 30 deletions(-) diff --git a/types/react-virtualized/react-virtualized-tests.tsx b/types/react-virtualized/react-virtualized-tests.tsx index 09a511dc8a..6c64fd42ef 100644 --- a/types/react-virtualized/react-virtualized-tests.tsx +++ b/types/react-virtualized/react-virtualized-tests.tsx @@ -89,6 +89,7 @@ export class ArrowKeyStepperExample extends PureComponent { import { List } from "react-virtualized"; export class AutoSizerExample extends PureComponent { + context: any; state: any; render() { const { list } = this.context; @@ -200,6 +201,7 @@ const GUTTER_SIZE = 3; const CELL_WIDTH = 75; export class CollectionExample extends PureComponent { + context: any; state: any; _columnYMap: any; @@ -372,6 +374,7 @@ export class ColumnSizerExample extends PureComponent { } export class GridExample extends PureComponent { + context: any; state = { columnCount: 1000, height: 300, @@ -511,7 +514,7 @@ const STATUS_LOADING = 1; const STATUS_LOADED = 2; export class InfiniteLoaderExample extends PureComponent { - + context: any; state: any; _timeoutIds = new Set(); @@ -619,6 +622,7 @@ export class InfiniteLoaderExample extends PureComponent { } export class ListExample extends PureComponent { + context: any; state: any; constructor(props: any, context: any) { super(props, context); @@ -758,6 +762,7 @@ export class GridExample2 extends PureComponent { _cellPositioner: Positioner; _masonry: Masonry; + context: any; state: any; constructor(props: any, context: any) { @@ -1256,6 +1261,7 @@ function mixColors(color1: any, color2: any, amount: any) { import { Column, Table, SortDirection, SortIndicator } from "react-virtualized"; export class TableExample extends PureComponent<{}, any> { + context: any; state = { disableHeader: false, headerHeight: 30, @@ -1498,6 +1504,7 @@ export class DynamicHeightTableColumnExample extends PureComponent { export class WindowScrollerExample extends PureComponent<{}, any> { _windowScroller: WindowScroller; + context: any; state = { showHeaderText: true }; diff --git a/types/react/index.d.ts b/types/react/index.d.ts index 17e9b466ae..a6421e8095 100644 --- a/types/react/index.d.ts +++ b/types/react/index.d.ts @@ -191,19 +191,19 @@ declare namespace React { ...children: ReactNode[]): DOMElement; // Custom components - function createElement

( + function createElement

( type: SFC

, props?: Attributes & P | null, ...children: ReactNode[]): SFCElement

; - function createElement

( + function createElement

( type: ClassType, ClassicComponentClass

>, props?: ClassAttributes> & P | null, ...children: ReactNode[]): CElement>; - function createElement, C extends ComponentClass

>( + function createElement

, C extends ComponentClass

>( type: ClassType, props?: ClassAttributes & P | null, ...children: ReactNode[]): CElement; - function createElement

( + function createElement

( type: SFC

| ComponentClass

| string, props?: Attributes & P | null, ...children: ReactNode[]): ReactElement

; @@ -255,11 +255,40 @@ declare namespace React { unstable_observedBits?: number; } - type Provider = ComponentType>; - type Consumer = ComponentType>; + // TODO: similar to how Fragment is actually a symbol, the values returned from createContext, + // forwardRef and memo are actually objects that are treated specially by the renderer; see: + // https://github.com/facebook/react/blob/v16.6.0/packages/react/src/ReactContext.js#L35-L48 + // https://github.com/facebook/react/blob/v16.6.0/packages/react/src/forwardRef.js#L42-L45 + // https://github.com/facebook/react/blob/v16.6.0/packages/react/src/memo.js#L27-L31 + // However, we have no way of telling the JSX parser that it's a JSX element type or its props other than + // by pretending to be a normal component. + // + // We don't just use ComponentType or SFC types because you are not supposed to attach statics to this + // object, but rather to the original function. + interface ExoticComponent

{ + /** + * **NOTE**: Exotic components are not callable. + */ + (props: P): (ReactElement|null); + readonly $$typeof: symbol; + } + + interface NamedExoticComponent

extends ExoticComponent

{ + displayName?: string; + } + + interface ProviderExoticComponent

extends ExoticComponent

{ + propTypes?: ValidationMap

; + } + + // NOTE: only the Context object itself can get a displayName + // https://github.com/facebook/react-devtools/blob/e0b854e4c/backend/attachRendererFiber.js#L310-L325 + type Provider = ProviderExoticComponent>; + type Consumer = ExoticComponent>; interface Context { Provider: Provider; Consumer: Consumer; + displayName?: string; } function createContext( defaultValue: T, @@ -269,8 +298,25 @@ declare namespace React { function isValidElement

(object: {} | null | undefined): object is ReactElement

; const Children: ReactChildren; - const Fragment: ComponentType; - const StrictMode: ComponentType; + const Fragment: ExoticComponent<{ children?: ReactNode }>; + const StrictMode: ExoticComponent<{ children?: ReactNode }>; + /** + * This feature is not yet available for server-side rendering. + * Suspense support will be added in a later release. + */ + const Suspense: ExoticComponent<{ + children?: ReactNode + + /** A fallback react tree to show when a Suspense child (like React.lazy) suspends */ + fallback: NonNullable|null + + // I tried looking at the code but I have no idea what it does. + // https://github.com/facebook/react/issues/13206#issuecomment-432489986 + /** + * Not implemented yet, requires unstable_ConcurrentMode + */ + // maxDuration?: number + }>; const version: string; // @@ -283,6 +329,29 @@ declare namespace React { // tslint:disable-next-line:no-empty-interface interface Component

extends ComponentLifecycle { } class Component { + // tslint won't let me format the sample code in a way that vscode likes it :( + /** + * If set, `this.context` will be set at runtime to the current value of the given Context. + * + * Usage: + * + * ```ts + * type MyContext = number + * const Ctx = React.createContext(0) + * + * class Foo extends React.Component { + * static contextType = Ctx + * context!: MyContext + * render () { + * return <>My context's value: {this.context}; + * } + * } + * ``` + * + * @see https://reactjs.org/docs/context.html#classcontexttype + */ + static contextType?: Context; + constructor(props: Readonly

); /** * @deprecated @@ -308,11 +377,6 @@ declare namespace React { // on the existence of `children` in `P`, then we should remove this. readonly props: Readonly<{ children?: ReactNode }> & Readonly

; state: Readonly; - /** - * @deprecated - * https://reactjs.org/docs/legacy-context.html - */ - context: any; /** * @deprecated * https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs @@ -417,6 +481,7 @@ declare namespace React { // Unfortunately, we have no way of declaring that the component constructor must implement this interface StaticLifecycle { getDerivedStateFromProps?: GetDerivedStateFromProps; + getDerivedStateFromError?: GetDerivedStateFromError; } type GetDerivedStateFromProps = @@ -427,6 +492,15 @@ declare namespace React { */ (nextProps: Readonly

, prevState: S) => Partial | null; + type GetDerivedStateFromError = + /** + * This lifecycle is invoked after an error has been thrown by a descendant component. + * It receives the error that was thrown as a parameter and should return a value to update state. + * + * Note: its presence prevents any of the deprecated lifecycle methods from being invoked + */ + (error: any) => Partial | null; + // This should be "infer SS" but can't use it yet interface NewLifecycle { /** @@ -558,7 +632,45 @@ declare namespace React { function createRef(): RefObject; - function forwardRef(Component: RefForwardingComponent): ComponentType

>; + // will show `ForwardRef(${Component.displayName || Component.name})` in devtools by default, + // but can be given its own specific name + interface ForwardRefExoticComponent

extends NamedExoticComponent

{ + defaultProps?: Partial

; + } + + function forwardRef(Component: RefForwardingComponent): ForwardRefExoticComponent

>; + + type ComponentProps> = + T extends ComponentType ? P : {}; + type ComponentPropsWithRef> = + T extends ComponentClass + ? P & ClassAttributes> + : T extends SFC + ? P + : {}; + + // will show `Memo(${Component.displayName || Component.name})` in devtools by default, + // but can be given its own specific name + interface MemoExoticComponent> extends NamedExoticComponent> { + readonly type: T; + } + + function memo

( + Component: SFC

, + propsAreEqual?: (prevProps: Readonly

, nextProps: Readonly

) => boolean + ): NamedExoticComponent

; + function memo>( + Component: T, + propsAreEqual?: (prevProps: Readonly>, nextProps: Readonly>) => boolean + ): MemoExoticComponent; + + interface LazyExoticComponent> extends ExoticComponent> { + readonly _result: T; + } + + function lazy>( + factory: () => Promise<{ default: T }> + ): LazyExoticComponent; // // React Hooks @@ -2460,6 +2572,14 @@ type Defaultize = P extends any & Partial>> : never; +type ReactManagedAttributes = C extends { propTypes: infer T; defaultProps: infer D; } + ? Defaultize>, D> + : C extends { propTypes: infer T; } + ? MergePropTypes> + : C extends { defaultProps: infer D; } + ? Defaultize + : P; + declare global { namespace JSX { // tslint:disable-next-line:no-empty-interface @@ -2470,13 +2590,13 @@ declare global { interface ElementAttributesProperty { props: {}; } interface ElementChildrenAttribute { children: {}; } - type LibraryManagedAttributes = C extends { propTypes: infer T; defaultProps: infer D; } - ? Defaultize>, D> - : C extends { propTypes: infer T; } - ? MergePropTypes> - : C extends { defaultProps: infer D; } - ? Defaultize - : P; + // We can't recurse forever because `type` can't be self-referential; + // let's assume it's reasonable to do a single React.lazy() around a single React.memo() / vice-versa + type LibraryManagedAttributes = C extends React.MemoExoticComponent | React.LazyExoticComponent + ? T extends React.MemoExoticComponent | React.LazyExoticComponent + ? ReactManagedAttributes + : ReactManagedAttributes + : ReactManagedAttributes; // tslint:disable-next-line:no-empty-interface interface IntrinsicAttributes extends React.Attributes { } diff --git a/types/react/test/index.ts b/types/react/test/index.ts index 6b9400918f..05e4232dd3 100644 --- a/types/react/test/index.ts +++ b/types/react/test/index.ts @@ -715,3 +715,18 @@ class RenderChildren extends React.Component { return children !== undefined ? children : null; } } + +const Memoized1 = React.memo(function Foo(props: { foo: string }) { return null; }); +React.createElement(Memoized1, { foo: 'string' }); + +const Memoized2 = React.memo( + function Bar(props: { bar: string }) { return null; }, + (prevProps, nextProps) => prevProps.bar === nextProps.bar +); +React.createElement(Memoized2, { bar: 'string' }); + +const specialSfc1: React.ExoticComponent = Memoized1; +const sfc: React.SFC = Memoized2; +// this $ExpectError is failing on TypeScript@next +// // $ExpectError Property '$$typeof' is missing in type +// const specialSfc2: React.SpecialSFC = props => null; diff --git a/types/react/test/managedAttributes.tsx b/types/react/test/managedAttributes.tsx index 56466e1cc8..1e0e8226d9 100644 --- a/types/react/test/managedAttributes.tsx +++ b/types/react/test/managedAttributes.tsx @@ -5,12 +5,12 @@ interface LeaveMeAloneDtslint { foo: string; } // import * as PropTypes from 'prop-types'; // interface Props { -// bool?: boolean -// fnc: () => any -// node?: React.ReactNode -// num?: number -// reqNode: NonNullable -// str: string +// bool?: boolean; +// fnc: () => any; +// node?: React.ReactNode; +// num?: number; +// reqNode: NonNullable; +// str: string; // } // const propTypes = { @@ -24,9 +24,9 @@ interface LeaveMeAloneDtslint { foo: string; } // }; // const defaultProps = { -// fnc: function() { return 'abc' } as () => any, +// fnc: (() => 'abc') as () => any, // extraBool: false, -// reqNode: 'text_node' as React.ReactNode +// reqNode: 'text_node' as NonNullable // }; // class AnnotatedPropTypesAndDefaultProps extends React.Component { @@ -158,3 +158,67 @@ interface LeaveMeAloneDtslint { foo: string; } // reqNode={} // /> // ]; + +// class ComponentWithNoDefaultProps extends React.Component {} + +// function FunctionalComponent(props: Props) { return <>{props.reqNode} } +// FunctionalComponent.defaultProps = defaultProps; + +// const functionalComponentTests = [ +// // $ExpectError +// , +// // This is possibly a bug/limitation of TS +// // Even if JSX.LibraryManagedAttributes returns the correct type, it doesn't seem to work with non-classes +// // This also doesn't work with things typed React.SFC

because defaultProps will always be Partial

+// // $ExpectError +// +// ]; + +// const MemoFunctionalComponent = React.memo(FunctionalComponent); +// const MemoAnnotatedDefaultProps = React.memo(AnnotatedDefaultProps); +// const LazyMemoFunctionalComponent = React.lazy(async () => ({ default: MemoFunctionalComponent })); +// const LazyMemoAnnotatedDefaultProps = React.lazy(async () => ({ default: MemoAnnotatedDefaultProps })); + +// const memoTests = [ +// // $ExpectError +// , +// // $ExpectError won't work as long as FunctionalComponent doesn't work either +// , +// // $ExpectError +// , +// , +// // $ExpectError this doesn't work despite JSX.LibraryManagedAttributes returning the correct type +// , +// // $ExpectError won't work as long as FunctionalComponent doesn't work either +// , +// // $ExpectError +// , +// // $ExpectError this doesn't work despite JSX.LibraryManagedAttributes returning the correct type +// +// ]; + +// type AnnotatedDefaultPropsLibraryManagedAttributes = JSX.LibraryManagedAttributes; +// // $ExpectType AnnotatedDefaultPropsLibraryManagedAttributes +// type FunctionalComponentLibraryManagedAttributes = JSX.LibraryManagedAttributes; +// // $ExpectType FunctionalComponentLibraryManagedAttributes +// type MemoFunctionalComponentLibraryManagedAttributes = JSX.LibraryManagedAttributes; +// // $ExpectType FunctionalComponentLibraryManagedAttributes +// type LazyMemoFunctionalComponentLibraryManagedAttributes = JSX.LibraryManagedAttributes; + +// const ForwardRef = React.forwardRef((props: Props, ref: React.Ref) => ( +// +// )); +// ForwardRef.defaultProps = defaultProps; + +// const forwardRefTests = [ +// // $ExpectError +// , +// } +// str='' +// />, +// // same bug as MemoFunctionalComponent and React.SFC-typed things +// // $ExpectError the type of ForwardRef.defaultProps stays Partial

anyway even if assigned +// +// ]; diff --git a/types/react/test/tsx.tsx b/types/react/test/tsx.tsx index 8f0e11aea5..79c1dd88b7 100644 --- a/types/react/test/tsx.tsx +++ b/types/react/test/tsx.tsx @@ -197,3 +197,60 @@ componentWithBadLifecycle.getSnapshotBeforeUpdate = () => { // $ExpectError componentWithBadLifecycle.componentDidUpdate = (prevProps: {}, prevState: {}, snapshot?: string) => { // $ExpectError return; }; + +const Memoized1 = React.memo(function Foo(props: { foo: string }) { return null; }); +; + +const Memoized2 = React.memo( + function Bar(props: { bar: string }) { return null; }, + (prevProps, nextProps) => prevProps.bar === nextProps.bar +); +; + +const Memoized3 = React.memo(class Test extends React.Component<{ x?: string }> {}); + { if (ref) { ref.props.x; } }}/>; + +const memoized4Ref = React.createRef(); +const Memoized4 = React.memo(React.forwardRef((props: {}, ref: React.Ref) =>

)); +; + +const Memoized5 = React.memo<{ test: boolean }>( + prop => <>{prop.test && prop.children}, + (prevProps, nextProps) => nextProps.test ? prevProps.children === nextProps.children : prevProps.test +); + +; + +// for some reason the ExpectType doesn't work if the type is namespaced +// $ExpectType NamedExoticComponent<{}> +const Memoized6 = React.memo(props => null); +; +// $ExpectError +; + +// NOTE: this test _requires_ TypeScript 3.1 +// It is passing, for what it's worth. +// const Memoized7 = React.memo((() => { +// function HasDefaultProps(props: { test: boolean }) { return null; } +// HasDefaultProps.defaultProps = { +// test: true +// }; +// return HasDefaultProps; +// })()); +// // $ExpectType boolean +// Memoized7.type.defaultProps.test; + +const LazyClassComponent = React.lazy(async () => ({ default: ComponentWithPropsAndState })); +const LazyMemoized3 = React.lazy(async () => ({ default: Memoized3 })); +const LazyRefForwarding = React.lazy(async () => ({ default: Memoized4 })); + +}> + + { if (ref) { ref.props.hello; } }} hello='test'/> + { if (ref) { ref.props.x; } }}/> + +; + +; +// $ExpectError +; diff --git a/types/theming/theming-tests.tsx b/types/theming/theming-tests.tsx index 18d24923bf..9f6ace6e54 100644 --- a/types/theming/theming-tests.tsx +++ b/types/theming/theming-tests.tsx @@ -54,6 +54,7 @@ function customWithTheme

( ) { return class CustomWithTheme extends React.Component { static contextTypes = themeListener.contextTypes; + context: any; setTheme = (theme: object) => this.setState({ theme }); subscription: number | undefined;