From b6cb635c6e4e24d6f171da55ebf43e5e4ce81305 Mon Sep 17 00:00:00 2001 From: Curtis Layne Date: Mon, 31 Jul 2017 16:48:04 -0400 Subject: [PATCH] [recompose] Fixes many serious issues with recompose types (#18496) The current iteration of recompose types have two issues: 1) Only a handful of HOCs infer required props from their children. 2) Even when an HOC does infer props from its children, it does not remove requirements for props it injects. This leads to the parent component rendering a wrapped commponent to not realize that props an HOC is injecting have already been handled and leads to a lot of typing issues. This PR updates a lot of the types to infer injected types and remove / partial them from the required props list that are passed to their parent. Due to a couple of typing issues in the TS language itself there are some missing pieces, namely compose cannot currently be typed. This PR, however, makes huge strides in correcting and inferring types in recompose. --- types/recompose/index.d.ts | 148 ++++++++++++++++++--------- types/recompose/recompose-tests.tsx | 151 ++++++++++++++++++++-------- 2 files changed, 205 insertions(+), 94 deletions(-) diff --git a/types/recompose/index.d.ts b/types/recompose/index.d.ts index 09c8d6a59d..3e006c175d 100644 --- a/types/recompose/index.d.ts +++ b/types/recompose/index.d.ts @@ -2,21 +2,25 @@ // Project: https://github.com/acdlite/recompose // Definitions by: Iskander Sierra // Samuel DeSota +// Curtis Layne // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.3 +// TypeScript Version: 2.4 /// declare module 'recompose' { import * as React from 'react'; - import { ComponentClass, StatelessComponent, ValidationMap } from 'react'; + import { ComponentType as Component, ComponentClass, StatelessComponent, ValidationMap } from 'react'; - type Component

= ComponentClass

| StatelessComponent

; type mapper = (input: TInner) => TOutter; type predicate = mapper; type predicateDiff = (current: T, next: T) => boolean + // Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766 + type Diff = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]; + type Omit = Pick>; + interface Observer{ next(props: T): void; complete(): void; @@ -33,43 +37,59 @@ declare module 'recompose' { interface ComponentEnhancer { (component: Component): ComponentClass; } - export interface InferableComponentEnhancer { - )>(component: TComp): TComp; + + // Injects props and removes them from the prop requirements. + // Will not pass through the injected props if they are passed in during + // render. Also adds new prop requirements from TNeedsProps. + export interface InferableComponentEnhancerWithProps { +

( + component: Component

+ ): React.ComponentType & TNeedsProps> } + // Injects props and removes them from the prop requirements. + // Will not pass through the injected props if they are passed in during + // render. + export type InferableComponentEnhancer = + InferableComponentEnhancerWithProps + + // Injects default props and makes them optional. Will still pass through + // the injected props if they are passed in during render. + export type DefaultingInferableComponentEnhancer = + InferableComponentEnhancerWithProps> // Higher-order components: https://github.com/acdlite/recompose/blob/master/docs/API.md#higher-order-components // mapProps: https://github.com/acdlite/recompose/blob/master/docs/API.md#mapprops export function mapProps( - propsMapper: mapper - ): ComponentEnhancer; + propsMapper: mapper, + ): InferableComponentEnhancerWithProps; // withProps: https://github.com/acdlite/recompose/blob/master/docs/API.md#withprops export function withProps( createProps: TInner | mapper - ): ComponentEnhancer; + ): InferableComponentEnhancerWithProps; // withPropsOnChange: https://github.com/acdlite/recompose/blob/master/docs/API.md#withpropsonchange export function withPropsOnChange( shouldMapOrKeys: string[] | predicateDiff, createProps: mapper - ): ComponentEnhancer; + ): InferableComponentEnhancerWithProps; // withHandlers: https://github.com/acdlite/recompose/blob/master/docs/API.md#withhandlers type EventHandler = Function; type HandleCreators = { [handlerName: string]: mapper; }; - type HandleCreatorsFactory = (initialProps: TOutter) => HandleCreators; - export function withHandlers( - handlerCreators: HandleCreators | HandleCreatorsFactory - ): ComponentEnhancer; + type HandleCreatorsFactory = (initialProps: TOutter) => HandleCreators; + export function withHandlers( + handlerCreators: HandleCreators | HandleCreatorsFactory + ): InferableComponentEnhancerWithProps; // defaultProps: https://github.com/acdlite/recompose/blob/master/docs/API.md#defaultprops - export function defaultProps( - props: Object - ): InferableComponentEnhancer; + export function defaultProps( + props: T + ): DefaultingInferableComponentEnhancer; // renameProp: https://github.com/acdlite/recompose/blob/master/docs/API.md#renameProp export function renameProp( @@ -90,69 +110,97 @@ declare module 'recompose' { ): ComponentEnhancer; // withState: https://github.com/acdlite/recompose/blob/master/docs/API.md#withState - export function withState( - stateName: string, - stateUpdaterName: string, - initialState: any | mapper - ): ComponentEnhancer void }*/, TOutter>; + type stateProps< + TState, + TStateName extends string, + TStateUpdaterName extends string + > = ( + {[stateName in TStateName]: TState} & + {[stateUpdateName in TStateUpdaterName]: (state: TState) => TState} + ) + export function withState< + TOutter, + TState, + TStateName extends string, + TStateUpdaterName extends string + >( + stateName: TStateName, + stateUpdaterName: TStateUpdaterName, + initialState: TState | mapper + ): InferableComponentEnhancerWithProps< + stateProps, + TOutter + >; // withReducer: https://github.com/acdlite/recompose/blob/master/docs/API.md#withReducer type reducer = (s: TState, a: TAction) => TState; - export function withReducer( - stateName: string, - dispatchName: string, + type reducerProps< + TState, + TAction, + TStateName extends string, + TDispatchName extends string + > = ( + {[stateName in TStateName]: TState} & + {[dispatchName in TDispatchName]: (a: TAction) => void} + ) + export function withReducer< + TOutter, + TState, + TAction, + TStateName extends string, + TDispatchName extends string + >( + stateName: TStateName, + dispatchName: TDispatchName, reducer: reducer, - initialState: TState - ): ComponentEnhancer; - export function withReducer( - stateName: string, - dispatchName: string, - reducer: reducer, - initialState: (props: TOutter) => TState - ): ComponentEnhancer; + initialState: TState | mapper + ): InferableComponentEnhancerWithProps< + reducerProps, + TOutter + >; // branch: https://github.com/acdlite/recompose/blob/master/docs/API.md#branch export function branch( test: predicate, - trueEnhancer: InferableComponentEnhancer, - falseEnhancer?: InferableComponentEnhancer + trueEnhancer: ComponentEnhancer | InferableComponentEnhancer<{}>, + falseEnhancer?: ComponentEnhancer | InferableComponentEnhancer<{}> ): ComponentEnhancer; // renderComponent: https://github.com/acdlite/recompose/blob/master/docs/API.md#renderComponent - export function renderComponent( - component: string | Component + export function renderComponent( + component: string | Component ): ComponentEnhancer; // renderNothing: https://github.com/acdlite/recompose/blob/master/docs/API.md#renderNothing - export const renderNothing: InferableComponentEnhancer; + export const renderNothing: InferableComponentEnhancer<{}>; // shouldUpdate: https://github.com/acdlite/recompose/blob/master/docs/API.md#shouldUpdate export function shouldUpdate( test: predicateDiff - ): InferableComponentEnhancer; + ): InferableComponentEnhancer<{}>; // pure: https://github.com/acdlite/recompose/blob/master/docs/API.md#pure - export function pure)> - (component: TComp): TComp; + export function pure + (component: Component): Component; // onlyUpdateForKeys: https://github.com/acdlite/recompose/blob/master/docs/API.md#onlyUpdateForKeys export function onlyUpdateForKeys( propKeys: Array - ) : InferableComponentEnhancer; + ) : InferableComponentEnhancer<{}>; // onlyUpdateForPropTypes: https://github.com/acdlite/recompose/blob/master/docs/API.md#onlyUpdateForPropTypes - export const onlyUpdateForPropTypes: InferableComponentEnhancer; + export const onlyUpdateForPropTypes: InferableComponentEnhancer<{}>; // withContext: https://github.com/acdlite/recompose/blob/master/docs/API.md#withContext export function withContext( childContextTypes: ValidationMap, getChildContext: mapper - ) : InferableComponentEnhancer; + ) : InferableComponentEnhancer<{}>; // getContext: https://github.com/acdlite/recompose/blob/master/docs/API.md#getContext - export function getContext( + export function getContext( contextTypes: ValidationMap - ) : InferableComponentEnhancer; + ) : InferableComponentEnhancer; interface ReactLifeCycleFunctionsThisArguments { props: TProps, @@ -180,10 +228,10 @@ declare module 'recompose' { export function lifecycle( spec: ReactLifeCycleFunctions - ): InferableComponentEnhancer; + ): InferableComponentEnhancer<{}>; // toClass: https://github.com/acdlite/recompose/blob/master/docs/API.md#toClass - export const toClass: InferableComponentEnhancer; + export const toClass: InferableComponentEnhancer<{}>; // Static property helpers: https://github.com/acdlite/recompose/blob/master/docs/API.md#static-property-helpers @@ -267,9 +315,9 @@ declare module 'recompose' { ): React.ComponentClass; // ??? // hoistStatics: https://github.com/acdlite/recompose/blob/master/docs/API.md#hoistStatics - export function hoistStatics( - hoc: InferableComponentEnhancer - ): InferableComponentEnhancer; + export function hoistStatics( + hoc: InferableComponentEnhancer + ): InferableComponentEnhancer; diff --git a/types/recompose/recompose-tests.tsx b/types/recompose/recompose-tests.tsx index a8bf57ee74..4ac39cc624 100644 --- a/types/recompose/recompose-tests.tsx +++ b/types/recompose/recompose-tests.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { - Component, // Higher-order components mapProps, withProps, withPropsOnChange, withHandlers, defaultProps, renameProp, renameProps, flattenProp, @@ -27,67 +26,107 @@ import baconConfig from "recompose/baconObservableConfig"; import kefirConfig from "recompose/kefirObservableConfig"; function testMapProps() { - interface InnerProps { inn: number } + interface InnerProps { + inn: number + other: string + } interface OutterProps { out: string } - const innerComponent = ({inn}: InnerProps) =>

{inn}
; + const InnerComponent = ({inn}: InnerProps) =>
{inn}
; - const enhancer = mapProps((props: OutterProps) => ({ inn: 123 } as InnerProps)); - const enhanced: React.ComponentClass = enhancer(innerComponent); + const enhancer = mapProps((props: OutterProps) => ({ inn: 123 })); + const Enhanced = enhancer(InnerComponent); + const rendered = ( + + ) } function testWithProps() { interface InnerProps { inn: number } - interface OutterProps { out: string } - const innerComponent = ({inn}: InnerProps) =>
{inn}
; + interface OutterProps { out: number } + const InnerComponent = ({inn}: InnerProps) =>
{inn}
; - const enhancer = withProps((props: OutterProps) => ({ inn: 123 } as InnerProps)); - const enhanced: React.ComponentClass = enhancer(innerComponent); + const enhancer = withProps((props: OutterProps) => ({ inn: props.out })); + const Enhanced = enhancer(InnerComponent); + const rendered = ( + + ) - const enhancer2 = withProps({ inn: 123 } as InnerProps); - const enhanced2: React.ComponentClass = enhancer2(innerComponent); + const enhancer2 = withProps({ inn: 123 }); + const Enhanced2 = enhancer2(InnerComponent); + const Rendered2 = ( + + ) } function testWithPropsOnChange() { interface InnerProps { inn: number } - interface OutterProps { out: string } - const innerComponent = ({inn}: InnerProps) =>
{inn}
; + interface OutterProps { out: number } + const InnerComponent = ({inn}: InnerProps) =>
{inn}
; - const enhancer = withPropsOnChange( - (props: OutterProps, nextProps: OutterProps) => true, - (props: OutterProps) => ({ inn: 123 } as InnerProps)); - const enhanced: React.ComponentClass = enhancer(innerComponent); + const enhancer = withProps((props: OutterProps) => ({ inn: props.out })); + const Enhanced = enhancer(InnerComponent); + const rendered = ( + + ) - const enhancer2 = withPropsOnChange( - [ "out" ], - (props: OutterProps) => ({ inn: 123 } as InnerProps)); - const enhanced2: React.ComponentClass = enhancer2(innerComponent); + const enhancer2 = withProps({ inn: 123 }); + const Enhanced2 = enhancer2(InnerComponent); + const Rendered2 = ( + + ) } function testWithHandlers() { - interface InnerProps { onSubmit: React.MouseEventHandler; onChange: Function; } - interface OutterProps { out: string } - const innerComponent = ({onChange, onSubmit}: InnerProps) => + interface InnerProps { + onSubmit: React.MouseEventHandler; + onChange: Function; + foo: string; + } + interface HandlerProps { + onSubmit: React.MouseEventHandler; + onChange: Function; + } + interface OutterProps { out: number; } + const InnerComponent: React.StatelessComponent = ({onChange, onSubmit}) =>
; - const enhancer = withHandlers({ - onChange: (props: OutterProps) => (e: any) => {}, - onSubmit: (props: OutterProps) => (e: any) => {}, + const enhancer = withHandlers({ + onChange: (props) => (e: any) => {}, + onSubmit: (props) => (e: React.MouseEvent) => {}, }); - const enhanced: React.ComponentClass = enhancer(innerComponent); + const Enhanced = enhancer(InnerComponent); + const rendered = ( + + ) - const enhancer2 = withHandlers((props: OutterProps) => ({ - onChange: (props: OutterProps) => (e: any) => {}, - onSubmit: (props: OutterProps) => (e: any) => {}, + const enhancer2 = withHandlers((props) => ({ + onChange: (props) => (e: any) => {}, + onSubmit: (props) => (e: React.MouseEvent) => {}, })); - const enhanced2: React.ComponentClass = enhancer2(innerComponent); + const Enhanced2 = enhancer2(InnerComponent); + const rendered2 = ( + + ) } function testDefaultProps() { - interface Props { a?: string; b?: number; } + interface Props { a: string; b: number; c: number; } const innerComponent = ({a, b}: Props) =>
{a}, {b}
; const enhancer = defaultProps({ a: "answer", b: 42 }); - const enhanced: React.StatelessComponent = enhancer JSX.Element>(innerComponent); + const Enhanced = enhancer(innerComponent); + const rendered = ( + + ) } function testRenameProp() { @@ -118,35 +157,59 @@ function testFlattenProp() { } function testWithState() { - interface InnerProps { count: number; setCount: (count: number) => void } + interface InnerProps { count: number; setCount: (count: number) => number } interface OutterProps { title: string } - const innerComponent: React.StatelessComponent = (props) => + const InnerComponent: React.StatelessComponent = (props) =>
props.setCount(0)}>
; - const enhancer = withState("count", "setCount", 0); - const enhanced: React.ComponentClass = enhancer(innerComponent); + // We can't infer types for TOutter with this form because + // Typescript only allows all or nothing + // when defining generics. You can't infer some and define other. + // For TOutter to be defined as not "{}" we would have to define + // all the generics. + const enhancer = withState("count", "setCount", 0); + const Enhanced = enhancer(InnerComponent); + const rendered = ( + + ); - const enhancer2 = withState("count", "setCount", + // Here we're able to infer TOutter since it's defined in the initial state + // function and Typescript is able to infer it from there. + const enhancer2 = withState("count", "setCount", (p: OutterProps) => p.title.length); - const enhanced2: React.ComponentClass = enhancer2(innerComponent); + const Enhanced2 = enhancer2(InnerComponent); + const rendered2 = ( + + ); } function testWithReducer() { interface State { count: number } interface Action { type: string } interface InnerProps { title: string; count: number; dispatch: (a: Action) => void; } - interface OutterProps { title: string; } - const innerComponent: React.StatelessComponent = (props: InnerProps) => + interface OutterProps { title: string; bar: number; } + const InnerComponent: React.StatelessComponent = (props) =>
props.dispatch({type: "INCREMENT"})}>
; + // Same issue here inferring TOutter as with the "withState" form. const enhancer = withReducer("count", "dispatch", (s: number, a: Action) => s + 1, 0); - const enhanced: React.ComponentClass = enhancer(innerComponent); + const Enhanced = enhancer(InnerComponent); + const rendered = ( + + ); + // Here we successfully infer TOutter from the initial state function const enhancer2 = withReducer("count", "dispatch", (s: number, a: Action) => s + 1, (props: OutterProps) => props.title.length); - const enhanced2: React.ComponentClass = enhancer2(innerComponent); + const Enhanced2 = enhancer2(InnerComponent); + const rendered2 = ( + + ); } function testBranch() {