diff --git a/src/components/connect.tsx b/src/components/connect.tsx index 963d72de9..a946a70db 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -1,17 +1,17 @@ /* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */ import hoistStatics from 'hoist-non-react-statics' -import React, { useContext, useMemo, useRef } from 'react' +import React, { ComponentType, useContext, useMemo, useRef } from 'react' import { isValidElementType, isContextConsumer } from 'react-is' import type { Store } from 'redux' import type { - AdvancedComponentDecorator, ConnectedComponent, InferableComponentEnhancer, InferableComponentEnhancerWithProps, ResolveThunks, DispatchProp, + ConnectPropsMaybeWithoutContext, } from '../types' import defaultSelectorFactory, { @@ -470,18 +470,18 @@ function connect< const Context = context - type WrappedComponentProps = TOwnProps & ConnectProps - const initMapStateToProps = mapStateToPropsFactory(mapStateToProps) const initMapDispatchToProps = mapDispatchToPropsFactory(mapDispatchToProps) const initMergeProps = mergePropsFactory(mergeProps) const shouldHandleStateChanges = Boolean(mapStateToProps) - const wrapWithConnect: AdvancedComponentDecorator< - TOwnProps, - WrappedComponentProps - > = (WrappedComponent) => { + const wrapWithConnect = ( + WrappedComponent: ComponentType + ) => { + type WrappedComponentProps = TProps & + ConnectPropsMaybeWithoutContext + if ( process.env.NODE_ENV !== 'production' && !isValidElementType(WrappedComponent) diff --git a/src/types.ts b/src/types.ts index 63d6d521c..5a8017d1e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -27,10 +27,6 @@ export interface DispatchProp { dispatch: Dispatch } -export type AdvancedComponentDecorator = ( - component: ComponentType -) => ComponentType - /** * A property P will be present if: * - it is present in DecorationTargetProps @@ -94,22 +90,34 @@ export type ConnectedComponent< WrappedComponent: C } +export type ConnectPropsMaybeWithoutContext = + TActualOwnProps extends { context: any } + ? Omit + : ConnectProps + +type Identity = T +export type Mapped = Identity<{ [k in keyof T]: T[k] }> + // 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. -// Uses distributive omit to preserve discriminated unions part of original prop type +// Uses distributive omit to preserve discriminated unions part of original prop type. +// Note> Most of the time TNeedsProps is empty, because the overloads in `Connect` +// just pass in `{}`. The real props we need come from the component. export type InferableComponentEnhancerWithProps = < C extends ComponentType>> >( component: C ) => ConnectedComponent< C, - DistributiveOmit< - GetLibraryManagedProps, - keyof Shared> - > & - TNeedsProps & - ConnectProps + Mapped< + DistributiveOmit< + GetLibraryManagedProps, + keyof Shared> + > & + TNeedsProps & + ConnectPropsMaybeWithoutContext> + > > // Injects props and removes them from the prop requirements. diff --git a/test/typetests/connect-options-and-issues.tsx b/test/typetests/connect-options-and-issues.tsx index 1f05c77d6..9e1260e8f 100644 --- a/test/typetests/connect-options-and-issues.tsx +++ b/test/typetests/connect-options-and-issues.tsx @@ -32,6 +32,7 @@ import { createStoreHook, TypedUseSelectorHook, } from '../../src/index' +import { ConnectPropsMaybeWithoutContext } from '../../src/types' import { expectType } from '../typeTestHelpers' @@ -464,7 +465,7 @@ function TestOptionalPropsMergedCorrectly() { } } - connect(mapStateToProps, mapDispatchToProps)(Component) + const Connected = connect(mapStateToProps, mapDispatchToProps)(Component) } function TestMoreGeneralDecorationProps() { @@ -881,3 +882,39 @@ function testPreserveDiscriminatedUnions() { ; ; } + +function issue1187ConnectAcceptsPropNamedContext() { + const mapStateToProps = (state: { name: string }) => { + return { + name: state.name, + } + } + + const connector = connect(mapStateToProps) + + type PropsFromRedux = ConnectedProps + + interface IButtonOwnProps { + label: string + context: 'LIST' | 'CARD' + } + type IButtonProps = IButtonOwnProps & PropsFromRedux + + function Button(props: IButtonProps) { + const { name, label, context } = props + return ( + + ) + } + + const ConnectedButton = connector(Button) + + // Since `IButtonOwnProps` includes a field named `context`, the final + // connected component _should_ use exactly that type, and omit the + // built-in `context: ReactReduxContext` field definition. + // If the types are broken, then `context` will have an error like: + // Type '"LIST"' is not assignable to type '("LIST" | "CARD") & (Context> | undefined)' + return +}