[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.
This commit is contained in:
Curtis Layne
2017-07-31 16:48:04 -04:00
committed by Sheetal Nandi
parent b629f5171e
commit b6cb635c6e
2 changed files with 205 additions and 94 deletions

View File

@@ -2,21 +2,25 @@
// Project: https://github.com/acdlite/recompose
// Definitions by: Iskander Sierra <https://github.com/iskandersierra>
// Samuel DeSota <https://github.com/mrapogee>
// Curtis Layne <https://github.com/clayne11>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
// TypeScript Version: 2.4
///<reference types="react" />
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<P> = ComponentClass<P> | StatelessComponent<P>;
type mapper<TInner, TOutter> = (input: TInner) => TOutter;
type predicate<T> = mapper<T, boolean>;
type predicateDiff<T> = (current: T, next: T) => boolean
// Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766
type Diff<T extends string, U extends string> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = Pick<T, Diff<keyof T, K>>;
interface Observer<T>{
next(props: T): void;
complete(): void;
@@ -33,43 +37,59 @@ declare module 'recompose' {
interface ComponentEnhancer<TInner, TOutter> {
(component: Component<TInner>): ComponentClass<TOutter>;
}
export interface InferableComponentEnhancer {
<P, TComp extends (Component<P>)>(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<TInjectedProps, TNeedsProps> {
<P extends TInjectedProps>(
component: Component<P>
): React.ComponentType<Omit<P, keyof TInjectedProps> & 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<TInjectedProps> =
InferableComponentEnhancerWithProps<TInjectedProps, {}>
// Injects default props and makes them optional. Will still pass through
// the injected props if they are passed in during render.
export type DefaultingInferableComponentEnhancer<TInjectedProps> =
InferableComponentEnhancerWithProps<TInjectedProps, Partial<TInjectedProps>>
// 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<TInner, TOutter>(
propsMapper: mapper<TOutter, TInner>
): ComponentEnhancer<TInner, TOutter>;
propsMapper: mapper<TOutter, TInner>,
): InferableComponentEnhancerWithProps<TInner, TOutter>;
// withProps: https://github.com/acdlite/recompose/blob/master/docs/API.md#withprops
export function withProps<TInner, TOutter>(
createProps: TInner | mapper<TOutter, TInner>
): ComponentEnhancer<TInner & TOutter, TOutter>;
): InferableComponentEnhancerWithProps<TInner & TOutter, TOutter>;
// withPropsOnChange: https://github.com/acdlite/recompose/blob/master/docs/API.md#withpropsonchange
export function withPropsOnChange<TInner, TOutter>(
shouldMapOrKeys: string[] | predicateDiff<TOutter>,
createProps: mapper<TOutter, TInner>
): ComponentEnhancer<TInner & TOutter, TOutter>;
): InferableComponentEnhancerWithProps<TInner & TOutter, TOutter>;
// withHandlers: https://github.com/acdlite/recompose/blob/master/docs/API.md#withhandlers
type EventHandler = Function;
type HandleCreators<TOutter> = {
[handlerName: string]: mapper<TOutter, EventHandler>;
};
type HandleCreatorsFactory<TOutter> = (initialProps: TOutter) => HandleCreators<TOutter>;
export function withHandlers<TInner, TOutter>(
handlerCreators: HandleCreators<TOutter> | HandleCreatorsFactory<TOutter>
): ComponentEnhancer<TInner, TOutter>;
type HandleCreatorsFactory<TOutter, THandlers> = (initialProps: TOutter) => HandleCreators<TOutter>;
export function withHandlers<TOutter, THandlers>(
handlerCreators: HandleCreators<TOutter> | HandleCreatorsFactory<TOutter, THandlers>
): InferableComponentEnhancerWithProps<THandlers, TOutter>;
// defaultProps: https://github.com/acdlite/recompose/blob/master/docs/API.md#defaultprops
export function defaultProps(
props: Object
): InferableComponentEnhancer;
export function defaultProps<T = {}>(
props: T
): DefaultingInferableComponentEnhancer<T>;
// renameProp: https://github.com/acdlite/recompose/blob/master/docs/API.md#renameProp
export function renameProp(
@@ -90,69 +110,97 @@ declare module 'recompose' {
): ComponentEnhancer<any, any>;
// withState: https://github.com/acdlite/recompose/blob/master/docs/API.md#withState
export function withState<TOutter>(
stateName: string,
stateUpdaterName: string,
initialState: any | mapper<TOutter, any>
): ComponentEnhancer<TOutter /*& { [stateName]: any; [stateUpdaterName]: (s: any) => 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<TOutter, TState>
): InferableComponentEnhancerWithProps<
stateProps<TState, TStateName, TStateUpdaterName>,
TOutter
>;
// withReducer: https://github.com/acdlite/recompose/blob/master/docs/API.md#withReducer
type reducer<TState, TAction> = (s: TState, a: TAction) => TState;
export function withReducer<TState, TAction>(
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<TState, TAction>,
initialState: TState
): ComponentEnhancer<any, any>;
export function withReducer<TOutter, TState, TAction>(
stateName: string,
dispatchName: string,
reducer: reducer<TState, TAction>,
initialState: (props: TOutter) => TState
): ComponentEnhancer<any, TOutter>;
initialState: TState | mapper<TOutter, TState>
): InferableComponentEnhancerWithProps<
reducerProps<TState, TAction, TStateName, TDispatchName>,
TOutter
>;
// branch: https://github.com/acdlite/recompose/blob/master/docs/API.md#branch
export function branch<TOutter>(
test: predicate<TOutter>,
trueEnhancer: InferableComponentEnhancer,
falseEnhancer?: InferableComponentEnhancer
trueEnhancer: ComponentEnhancer<any, any> | InferableComponentEnhancer<{}>,
falseEnhancer?: ComponentEnhancer<any, any> | InferableComponentEnhancer<{}>
): ComponentEnhancer<any, TOutter>;
// renderComponent: https://github.com/acdlite/recompose/blob/master/docs/API.md#renderComponent
export function renderComponent(
component: string | Component<any>
export function renderComponent<TProps>(
component: string | Component<TProps>
): ComponentEnhancer<any, any>;
// 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<TProps>(
test: predicateDiff<TProps>
): InferableComponentEnhancer;
): InferableComponentEnhancer<{}>;
// pure: https://github.com/acdlite/recompose/blob/master/docs/API.md#pure
export function pure<TProps, TComp extends (Component<TProps>)>
(component: TComp): TComp;
export function pure<TProps>
(component: Component<TProps>): Component<TProps>;
// onlyUpdateForKeys: https://github.com/acdlite/recompose/blob/master/docs/API.md#onlyUpdateForKeys
export function onlyUpdateForKeys(
propKeys: Array<string>
) : 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<TContext, TProps>(
childContextTypes: ValidationMap<TContext>,
getChildContext: mapper<TProps, any>
) : InferableComponentEnhancer;
) : InferableComponentEnhancer<{}>;
// getContext: https://github.com/acdlite/recompose/blob/master/docs/API.md#getContext
export function getContext<TContext, TProps>(
export function getContext<TContext>(
contextTypes: ValidationMap<TContext>
) : InferableComponentEnhancer;
) : InferableComponentEnhancer<TContext>;
interface ReactLifeCycleFunctionsThisArguments<TProps, TState> {
props: TProps,
@@ -180,10 +228,10 @@ declare module 'recompose' {
export function lifecycle<TProps, TState>(
spec: ReactLifeCycleFunctions<TProps, TState>
): 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<any>; // ???
// hoistStatics: https://github.com/acdlite/recompose/blob/master/docs/API.md#hoistStatics
export function hoistStatics(
hoc: InferableComponentEnhancer
): InferableComponentEnhancer;
export function hoistStatics<TProps>(
hoc: InferableComponentEnhancer<TProps>
): InferableComponentEnhancer<TProps>;

View File

@@ -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) => <div>{inn}</div>;
const InnerComponent = ({inn}: InnerProps) => <div>{inn}</div>;
const enhancer = mapProps((props: OutterProps) => ({ inn: 123 } as InnerProps));
const enhanced: React.ComponentClass<OutterProps> = enhancer(innerComponent);
const enhancer = mapProps((props: OutterProps) => ({ inn: 123 }));
const Enhanced = enhancer(InnerComponent);
const rendered = (
<Enhanced
other='foo'
out='bar'
/>
)
}
function testWithProps() {
interface InnerProps { inn: number }
interface OutterProps { out: string }
const innerComponent = ({inn}: InnerProps) => <div>{inn}</div>;
interface OutterProps { out: number }
const InnerComponent = ({inn}: InnerProps) => <div>{inn}</div>;
const enhancer = withProps((props: OutterProps) => ({ inn: 123 } as InnerProps));
const enhanced: React.ComponentClass<OutterProps> = enhancer(innerComponent);
const enhancer = withProps((props: OutterProps) => ({ inn: props.out }));
const Enhanced = enhancer(InnerComponent);
const rendered = (
<Enhanced out={123}/>
)
const enhancer2 = withProps<InnerProps, OutterProps>({ inn: 123 } as InnerProps);
const enhanced2: React.ComponentClass<OutterProps> = enhancer2(innerComponent);
const enhancer2 = withProps({ inn: 123 });
const Enhanced2 = enhancer2(InnerComponent);
const Rendered2 = (
<Enhanced2/>
)
}
function testWithPropsOnChange() {
interface InnerProps { inn: number }
interface OutterProps { out: string }
const innerComponent = ({inn}: InnerProps) => <div>{inn}</div>;
interface OutterProps { out: number }
const InnerComponent = ({inn}: InnerProps) => <div>{inn}</div>;
const enhancer = withPropsOnChange(
(props: OutterProps, nextProps: OutterProps) => true,
(props: OutterProps) => ({ inn: 123 } as InnerProps));
const enhanced: React.ComponentClass<OutterProps> = enhancer(innerComponent);
const enhancer = withProps((props: OutterProps) => ({ inn: props.out }));
const Enhanced = enhancer(InnerComponent);
const rendered = (
<Enhanced out={123}/>
)
const enhancer2 = withPropsOnChange(
[ "out" ],
(props: OutterProps) => ({ inn: 123 } as InnerProps));
const enhanced2: React.ComponentClass<OutterProps> = enhancer2(innerComponent);
const enhancer2 = withProps({ inn: 123 });
const Enhanced2 = enhancer2(InnerComponent);
const Rendered2 = (
<Enhanced2/>
)
}
function testWithHandlers() {
interface InnerProps { onSubmit: React.MouseEventHandler<HTMLDivElement>; onChange: Function; }
interface OutterProps { out: string }
const innerComponent = ({onChange, onSubmit}: InnerProps) =>
interface InnerProps {
onSubmit: React.MouseEventHandler<HTMLDivElement>;
onChange: Function;
foo: string;
}
interface HandlerProps {
onSubmit: React.MouseEventHandler<HTMLDivElement>;
onChange: Function;
}
interface OutterProps { out: number; }
const InnerComponent: React.StatelessComponent<InnerProps> = ({onChange, onSubmit}) =>
<div onClick={onSubmit}></div>;
const enhancer = withHandlers<InnerProps, OutterProps>({
onChange: (props: OutterProps) => (e: any) => {},
onSubmit: (props: OutterProps) => (e: any) => {},
const enhancer = withHandlers<OutterProps, HandlerProps>({
onChange: (props) => (e: any) => {},
onSubmit: (props) => (e: React.MouseEvent<any>) => {},
});
const enhanced: React.ComponentClass<OutterProps> = enhancer(innerComponent);
const Enhanced = enhancer(InnerComponent);
const rendered = (
<Enhanced
foo="bar"
out={42}
/>
)
const enhancer2 = withHandlers<InnerProps, OutterProps>((props: OutterProps) => ({
onChange: (props: OutterProps) => (e: any) => {},
onSubmit: (props: OutterProps) => (e: any) => {},
const enhancer2 = withHandlers<OutterProps, HandlerProps>((props) => ({
onChange: (props) => (e: any) => {},
onSubmit: (props) => (e: React.MouseEvent<any>) => {},
}));
const enhanced2: React.ComponentClass<OutterProps> = enhancer2(innerComponent);
const Enhanced2 = enhancer2(InnerComponent);
const rendered2 = (
<Enhanced2
foo="bar"
out={42}
/>
)
}
function testDefaultProps() {
interface Props { a?: string; b?: number; }
interface Props { a: string; b: number; c: number; }
const innerComponent = ({a, b}: Props) => <div>{a}, {b}</div>;
const enhancer = defaultProps({ a: "answer", b: 42 });
const enhanced: React.StatelessComponent<Props> = enhancer<Props, ({a, b}: Props) => JSX.Element>(innerComponent);
const Enhanced = enhancer(innerComponent);
const rendered = (
<Enhanced c={42} />
)
}
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<OutterProps & InnerProps> = (props) =>
const InnerComponent: React.StatelessComponent<InnerProps> = (props) =>
<div onClick={() => props.setCount(0)}></div>;
const enhancer = withState<OutterProps>("count", "setCount", 0);
const enhanced: React.ComponentClass<OutterProps> = 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 = (
<Enhanced />
);
const enhancer2 = withState<OutterProps>("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<OutterProps> = enhancer2(innerComponent);
const Enhanced2 = enhancer2(InnerComponent);
const rendered2 = (
<Enhanced2 title="foo" />
);
}
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<InnerProps> = (props: InnerProps) =>
interface OutterProps { title: string; bar: number; }
const InnerComponent: React.StatelessComponent<InnerProps> = (props) =>
<div onClick={() => props.dispatch({type: "INCREMENT"})}></div>;
// 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<OutterProps> = enhancer(innerComponent);
const Enhanced = enhancer(InnerComponent);
const rendered = (
<Enhanced title="foo"/>
);
// 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<OutterProps> = enhancer2(innerComponent);
const Enhanced2 = enhancer2(InnerComponent);
const rendered2 = (
<Enhanced2
title="foo"
bar={42}
/>
);
}
function testBranch() {