Skip to content

Commit f40d82d

Browse files
committed
Omit built-in context prop if user component props include context
Connected components allow users to pass in a `ReactReduxContext` instance as a prop named `context`. We also do internal checks to see if that really _is_ a context instance, in case the user passed a real value as `props.context`. However, the TS types did not reflect this - `props.context` was always a `ReactReduxContext` type, and if users did have a component that accepted a prop named `context`, this would cause a type clash. In this case, the types didn't reflect the reality of runtime behavior. By extracting "the props of the component" and omitting the built-in `context` field type if appropriate, this now works.
1 parent bbc546e commit f40d82d

File tree

3 files changed

+54
-14
lines changed

3 files changed

+54
-14
lines changed

src/components/connect.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */
22
import hoistStatics from 'hoist-non-react-statics'
3-
import React, { useContext, useMemo, useRef } from 'react'
3+
import React, { ComponentType, useContext, useMemo, useRef } from 'react'
44
import { isValidElementType, isContextConsumer } from 'react-is'
55

66
import type { Store } from 'redux'
77

88
import type {
9-
AdvancedComponentDecorator,
109
ConnectedComponent,
1110
InferableComponentEnhancer,
1211
InferableComponentEnhancerWithProps,
1312
ResolveThunks,
1413
DispatchProp,
14+
ConnectPropsMaybeWithoutContext,
1515
} from '../types'
1616

1717
import defaultSelectorFactory, {
@@ -465,18 +465,18 @@ function connect<
465465

466466
const Context = context
467467

468-
type WrappedComponentProps = TOwnProps & ConnectProps
469-
470468
const initMapStateToProps = mapStateToPropsFactory(mapStateToProps)
471469
const initMapDispatchToProps = mapDispatchToPropsFactory(mapDispatchToProps)
472470
const initMergeProps = mergePropsFactory(mergeProps)
473471

474472
const shouldHandleStateChanges = Boolean(mapStateToProps)
475473

476-
const wrapWithConnect: AdvancedComponentDecorator<
477-
TOwnProps,
478-
WrappedComponentProps
479-
> = (WrappedComponent) => {
474+
const wrapWithConnect = <TProps,>(
475+
WrappedComponent: ComponentType<TProps>
476+
) => {
477+
type WrappedComponentProps = TProps &
478+
ConnectPropsMaybeWithoutContext<TProps>
479+
480480
if (
481481
process.env.NODE_ENV !== 'production' &&
482482
!isValidElementType(WrappedComponent)

src/types.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ export interface DispatchProp<A extends Action = AnyAction> {
2525
dispatch: Dispatch<A>
2626
}
2727

28-
export type AdvancedComponentDecorator<TProps, TOwnProps> = (
29-
component: ComponentType<TProps>
30-
) => ComponentType<TOwnProps>
31-
3228
/**
3329
* A property P will be present if:
3430
* - it is present in DecorationTargetProps
@@ -92,10 +88,17 @@ export type ConnectedComponent<
9288
WrappedComponent: C
9389
}
9490

91+
export type ConnectPropsMaybeWithoutContext<TActualOwnProps> =
92+
TActualOwnProps extends { context: any }
93+
? Omit<ConnectProps, 'context'>
94+
: ConnectProps
95+
9596
// Injects props and removes them from the prop requirements.
9697
// Will not pass through the injected props if they are passed in during
9798
// render. Also adds new prop requirements from TNeedsProps.
98-
// Uses distributive omit to preserve discriminated unions part of original prop type
99+
// Uses distributive omit to preserve discriminated unions part of original prop type.
100+
// Note> Most of the time TNeedsProps is empty, because the overloads in `Connect`
101+
// just pass in `{}`. The real props we need come from the component.
99102
export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> = <
100103
C extends ComponentType<Matching<TInjectedProps, GetProps<C>>>
101104
>(
@@ -107,7 +110,7 @@ export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> = <
107110
keyof Shared<TInjectedProps, GetLibraryManagedProps<C>>
108111
> &
109112
TNeedsProps &
110-
ConnectProps
113+
ConnectPropsMaybeWithoutContext<TNeedsProps & GetProps<C>>
111114
>
112115

113116
// Injects props and removes them from the prop requirements.

test/typetests/connect-options-and-issues.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
createStoreHook,
3333
TypedUseSelectorHook,
3434
} from '../../src/index'
35+
import { ConnectPropsMaybeWithoutContext } from '../../src/types'
3536

3637
import { expectType } from '../typeTestHelpers'
3738

@@ -881,3 +882,39 @@ function testPreserveDiscriminatedUnions() {
881882
;<ConnectedMyText type="localized" color="red" />
882883
;<ConnectedMyText type="localized" color="red" params={someParams} />
883884
}
885+
886+
function issue1187ConnectAcceptsPropNamedContext() {
887+
const mapStateToProps = (state: { name: string }) => {
888+
return {
889+
name: state.name,
890+
}
891+
}
892+
893+
const connector = connect(mapStateToProps)
894+
895+
type PropsFromRedux = ConnectedProps<typeof connector>
896+
897+
interface IButtonOwnProps {
898+
label: string
899+
context: 'LIST' | 'CARD'
900+
}
901+
type IButtonProps = IButtonOwnProps & PropsFromRedux
902+
903+
function Button(props: IButtonProps) {
904+
const { name, label, context } = props
905+
return (
906+
<button>
907+
{name} - {label} - {context}
908+
</button>
909+
)
910+
}
911+
912+
const ConnectedButton = connector(Button)
913+
914+
// Since `IButtonOwnProps` includes a field named `context`, the final
915+
// connected component _should_ use exactly that type, and omit the
916+
// built-in `context: ReactReduxContext` field definition.
917+
// If the types are broken, then `context` will have an error like:
918+
// Type '"LIST"' is not assignable to type '("LIST" | "CARD") & (Context<ReactReduxContextValue<any, AnyAction>> | undefined)'
919+
return <ConnectedButton label="a" context="LIST" />
920+
}

0 commit comments

Comments
 (0)