Skip to content

Commit cb93374

Browse files
authored
Rework connect types and add type test setup (#1766)
1 parent 2c08811 commit cb93374

15 files changed

+1032
-83
lines changed

.eslintrc

+5-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@
3636
"react/jsx-uses-react": 1,
3737
"react/jsx-no-undef": 2,
3838
"react/jsx-wrap-multilines": 2,
39-
"react/no-string-refs": 0
39+
"react/no-string-refs": 0,
40+
"no-unused-vars": "off",
41+
"@typescript-eslint/no-unused-vars": ["error"],
42+
"no-redeclare": "off",
43+
"@typescript-eslint/no-redeclare": ["error"]
4044
},
4145
"plugins": ["@typescript-eslint", "import", "react"],
4246
"globals": {

.github/workflows/test.yml

+54
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,57 @@ jobs:
3535

3636
- name: Collect coverage
3737
run: yarn coverage
38+
39+
test-types:
40+
name: Test Types with TypeScript ${{ matrix.ts }}
41+
42+
needs: [build]
43+
runs-on: ubuntu-latest
44+
strategy:
45+
fail-fast: false
46+
matrix:
47+
node: ['14.x']
48+
ts: ['3.9', '4.0', '4.1', '4.2', '4.3', 'next']
49+
steps:
50+
- name: Checkout repo
51+
uses: actions/checkout@v2
52+
53+
- name: Use node ${{ matrix.node }}
54+
uses: actions/setup-node@v1
55+
with:
56+
node-version: ${{ matrix.node }}
57+
58+
- uses: actions/cache@v2
59+
with:
60+
path: .yarn/cache
61+
key: yarn-${{ hashFiles('yarn.lock') }}
62+
restore-keys: yarn-
63+
64+
- name: Install deps
65+
run: yarn install
66+
67+
- name: Install TypeScript ${{ matrix.ts }}
68+
run: yarn add typescript@${{ matrix.ts }}
69+
70+
# - uses: actions/download-artifact@v2
71+
# with:
72+
# name: package
73+
# path: packages/toolkit
74+
75+
# - name: Install build artifact
76+
# run: yarn add ./package.tgz
77+
78+
# - run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./jest.config.js ./src/tests/*.* ./src/query/tests/*.*
79+
80+
# - name: "@ts-ignore stuff that didn't exist pre-4.1 in the tests"
81+
# if: ${{ matrix.ts < 4.1 }}
82+
# run: sed -i -e 's/@pre41-ts-ignore/@ts-ignore/' -e '/pre41-remove-start/,/pre41-remove-end/d' ./src/tests/*.* ./src/query/tests/*.ts*
83+
84+
# - name: 'disable strictOptionalProperties'
85+
# if: ${{ matrix.ts == 'next' }}
86+
# run: sed -i -e 's|//\(.*strictOptionalProperties.*\)$|\1|' tsconfig.base.json
87+
88+
- name: Test types
89+
run: |
90+
yarn tsc --version
91+
yarn type-tests

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ dist
33
lib
44
coverage
55
es
6+
temp/
7+
react-redux-*/
68

79
.cache
810
.yarnrc

etc/react-redux.api.md

+26-33
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
55
```ts
66

7-
/// <reference types="hoist-non-react-statics" />
87
/// <reference types="react" />
98

109
import { Action } from 'redux';
@@ -15,49 +14,40 @@ import { ComponentClass } from 'react';
1514
import { ComponentType } from 'react';
1615
import { Context } from 'react';
1716
import { Dispatch } from 'redux';
18-
import { ForwardRefExoticComponent } from 'react';
19-
import hoistStatics from 'hoist-non-react-statics';
20-
import { MemoExoticComponent } from 'react';
21-
import { NamedExoticComponent } from 'react';
22-
import { NonReactStatics } from 'hoist-non-react-statics';
17+
import type { NonReactStatics } from 'hoist-non-react-statics';
2318
import { default as React_2 } from 'react';
2419
import { ReactNode } from 'react';
25-
import { RefAttributes } from 'react';
2620
import { Store } from 'redux';
2721

2822
// @public (undocumented)
29-
export type AdvancedComponentDecorator<TProps, TOwnProps> = (component: ComponentType<TProps>) => NamedExoticComponent<TOwnProps>;
23+
export type AdvancedComponentDecorator<TProps, TOwnProps> = (component: ComponentType<TProps>) => ComponentType<TOwnProps>;
3024

3125
// @public (undocumented)
3226
export type AnyIfEmpty<T extends object> = keyof T extends never ? any : T;
3327

3428
export { batch }
3529

3630
// @public (undocumented)
37-
export const connect: (mapStateToProps: MapStateToPropsParam<unknown, unknown, DefaultRootState>, mapDispatchToProps: unknown, mergeProps: MergeProps<unknown, unknown, unknown, unknown>, { pure, areStatesEqual, areOwnPropsEqual, areStatePropsEqual, areMergedPropsEqual, ...extraOptions }?: ConnectOptions<DefaultRootState, {}, {}, {}>) => <WC extends ComponentType< {}>>(WrappedComponent: WC) => (ForwardRefExoticComponent<RefAttributes<unknown>> & {
38-
WrappedComponent: WC;
39-
} & NonReactStatics<WC, {}>) | ((({
40-
<TOwnProps>(props: ConnectProps & TOwnProps): JSX.Element;
41-
displayName: string;
42-
} | MemoExoticComponent< {
43-
<TOwnProps>(props: ConnectProps & TOwnProps): JSX.Element;
44-
displayName: string;
45-
}>) & {
46-
WrappedComponent: WC;
47-
}) & NonReactStatics<WC, {}>);
48-
49-
// @public (undocumented)
50-
export function connectAdvanced<S, TProps, TOwnProps, TFactoryOptions extends AnyObject = {}>(selectorFactory: SelectorFactory<S, TProps, unknown, unknown>, { getDisplayName, methodName, shouldHandleStateChanges, forwardRef, context, ...connectOptions }?: ConnectAdvancedOptions & Partial<TFactoryOptions>): <WC extends React_2.ComponentType<{}>>(WrappedComponent: WC) => (React_2.ForwardRefExoticComponent<React_2.RefAttributes<unknown>> & {
51-
WrappedComponent: WC;
52-
} & hoistStatics.NonReactStatics<WC, {}>) | ((({
53-
<TOwnProps_1>(props: ConnectProps & TOwnProps_1): JSX.Element;
54-
displayName: string;
55-
} | React_2.MemoExoticComponent<{
56-
<TOwnProps_1>(props: ConnectProps & TOwnProps_1): JSX.Element;
57-
displayName: string;
58-
}>) & {
59-
WrappedComponent: WC;
60-
}) & hoistStatics.NonReactStatics<WC, {}>);
31+
export const connect: {
32+
(): InferableComponentEnhancer<DispatchProp>;
33+
<TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>): InferableComponentEnhancerWithProps<TStateProps & DispatchProp<AnyAction>, TOwnProps>;
34+
<no_state = {}, TDispatchProps = {}, TOwnProps_1 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps_1>): InferableComponentEnhancerWithProps<TDispatchProps, TOwnProps_1>;
35+
<no_state_1 = {}, TDispatchProps_1 = {}, TOwnProps_2 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_1, TOwnProps_2>): InferableComponentEnhancerWithProps<ResolveThunks<TDispatchProps_1>, TOwnProps_2>;
36+
<TStateProps_1 = {}, TDispatchProps_2 = {}, TOwnProps_3 = {}, State_1 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_1, TOwnProps_3, State_1>, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps_2, TOwnProps_3>): InferableComponentEnhancerWithProps<TStateProps_1 & TDispatchProps_2, TOwnProps_3>;
37+
<TStateProps_2 = {}, TDispatchProps_3 = {}, TOwnProps_4 = {}, State_2 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_2, TOwnProps_4, State_2>, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_3, TOwnProps_4>): InferableComponentEnhancerWithProps<TStateProps_2 & ResolveThunks<TDispatchProps_3>, TOwnProps_4>;
38+
<no_state_2 = {}, no_dispatch_1 = {}, TOwnProps_5 = {}, TMergedProps = {}>(mapStateToProps: null | undefined, mapDispatchToProps: null | undefined, mergeProps: MergeProps<undefined, undefined, TOwnProps_5, TMergedProps>): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps_5>;
39+
<TStateProps_3 = {}, no_dispatch_2 = {}, TOwnProps_6 = {}, TMergedProps_1 = {}, State_3 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_3, TOwnProps_6, State_3>, mapDispatchToProps: null | undefined, mergeProps: MergeProps<TStateProps_3, undefined, TOwnProps_6, TMergedProps_1>): InferableComponentEnhancerWithProps<TMergedProps_1, TOwnProps_6>;
40+
<no_state_3 = {}, TDispatchProps_4 = {}, TOwnProps_7 = {}, TMergedProps_2 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_4, TOwnProps_7>, mergeProps: MergeProps<undefined, TDispatchProps_4, TOwnProps_7, TMergedProps_2>): InferableComponentEnhancerWithProps<TMergedProps_2, TOwnProps_7>;
41+
<TStateProps_4 = {}, no_dispatch_3 = {}, TOwnProps_8 = {}, State_4 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_4, TOwnProps_8, State_4>, mapDispatchToProps: null | undefined, mergeProps: null | undefined, options: ConnectOptions<State_4, TStateProps_4, TOwnProps_8, {}>): InferableComponentEnhancerWithProps<DispatchProp<AnyAction> & TStateProps_4, TOwnProps_8>;
42+
<TStateProps_5 = {}, TDispatchProps_5 = {}, TOwnProps_9 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps_5, TOwnProps_9>, mergeProps: null | undefined, options: ConnectOptions<{}, TStateProps_5, TOwnProps_9, {}>): InferableComponentEnhancerWithProps<TDispatchProps_5, TOwnProps_9>;
43+
<TStateProps_6 = {}, TDispatchProps_6 = {}, TOwnProps_10 = {}>(mapStateToProps: null | undefined, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_6, TOwnProps_10>, mergeProps: null | undefined, options: ConnectOptions<{}, TStateProps_6, TOwnProps_10, {}>): InferableComponentEnhancerWithProps<ResolveThunks<TDispatchProps_6>, TOwnProps_10>;
44+
<TStateProps_7 = {}, TDispatchProps_7 = {}, TOwnProps_11 = {}, State_5 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_7, TOwnProps_11, State_5>, mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps_7, TOwnProps_11>, mergeProps: null | undefined, options: ConnectOptions<State_5, TStateProps_7, TOwnProps_11, {}>): InferableComponentEnhancerWithProps<TStateProps_7 & TDispatchProps_7, TOwnProps_11>;
45+
<TStateProps_8 = {}, TDispatchProps_8 = {}, TOwnProps_12 = {}, State_6 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_8, TOwnProps_12, State_6>, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_8, TOwnProps_12>, mergeProps: null | undefined, options: ConnectOptions<State_6, TStateProps_8, TOwnProps_12, {}>): InferableComponentEnhancerWithProps<TStateProps_8 & ResolveThunks<TDispatchProps_8>, TOwnProps_12>;
46+
<TStateProps_9 = {}, TDispatchProps_9 = {}, TOwnProps_13 = {}, TMergedProps_3 = {}, State_7 = DefaultRootState>(mapStateToProps: MapStateToPropsParam<TStateProps_9, TOwnProps_13, State_7>, mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps_9, TOwnProps_13>, mergeProps: MergeProps<TStateProps_9, TDispatchProps_9, TOwnProps_13, TMergedProps_3>, options?: ConnectOptions<State_7, TStateProps_9, TOwnProps_13, TMergedProps_3> | undefined): InferableComponentEnhancerWithProps<TMergedProps_3, TOwnProps_13>;
47+
};
48+
49+
// @public (undocumented)
50+
export function connectAdvanced<S, TProps, TOwnProps, TFactoryOptions = {}>(selectorFactory: SelectorFactory<S, TProps, unknown, unknown>, { getDisplayName, methodName, shouldHandleStateChanges, forwardRef, context, ...connectOptions }?: ConnectAdvancedOptions & Partial<TFactoryOptions>): AdvancedComponentDecorator<TProps, TOwnProps & ConnectProps>;
6151

6252
// @public (undocumented)
6353
export interface ConnectAdvancedOptions {
@@ -76,10 +66,13 @@ export interface ConnectAdvancedOptions {
7666
}
7767

7868
// @public (undocumented)
79-
export type ConnectedComponent<C extends ComponentType<any>, P> = NamedExoticComponent<JSX.LibraryManagedAttributes<C, P>> & NonReactStatics<C> & {
69+
export type ConnectedComponent<C extends ComponentType<any>, P> = ComponentType<P> & NonReactStatics<C> & {
8070
WrappedComponent: C;
8171
};
8272

73+
// @public
74+
export type ConnectedProps<TConnector> = TConnector extends InferableComponentEnhancerWithProps<infer TInjectedProps, any> ? unknown extends TInjectedProps ? TConnector extends InferableComponentEnhancer<infer TInjectedProps> ? TInjectedProps : never : TInjectedProps : never;
75+
8376
// @public (undocumented)
8477
export interface ConnectProps {
8578
// (undocumented)

package.json

+7-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@
3030
"build:types": "tsc",
3131
"build": "yarn build:types && yarn build:commonjs && yarn build:es && yarn build:umd && yarn build:umd:min",
3232
"clean": "rimraf lib dist es coverage",
33-
"api-types": "api-extractor --local",
33+
"api-types": "api-extractor run --local",
3434
"format": "prettier --write \"{src,test}/**/*.{js,ts}\" \"docs/**/*.md\"",
3535
"lint": "eslint src --ext ts,js test/utils test/components test/hooks",
3636
"prepare": "yarn clean && yarn build",
3737
"pretest": "yarn lint",
3838
"test": "jest",
39+
"type-tests": "yarn tsc -p test/typetests",
3940
"coverage": "codecov"
4041
},
4142
"workspaces": [
@@ -54,7 +55,6 @@
5455
},
5556
"dependencies": {
5657
"@babel/runtime": "^7.12.1",
57-
"@types/react-redux": "^7.1.16",
5858
"hoist-non-react-statics": "^3.3.2",
5959
"loose-envify": "^1.4.0",
6060
"prop-types": "^15.7.2",
@@ -80,6 +80,11 @@
8080
"@testing-library/react": "^12.0.0",
8181
"@testing-library/react-hooks": "^3.4.2",
8282
"@testing-library/react-native": "^7.1.0",
83+
"@types/object-assign": "^4.0.30",
84+
"@types/react": "^17.0.14",
85+
"@types/react-dom": "^17.0.9",
86+
"@types/react-is": "^17.0.1",
87+
"@types/react-redux": "^7.1.18",
8388
"@typescript-eslint/eslint-plugin": "^4.28.0",
8489
"@typescript-eslint/parser": "^4.28.0",
8590
"babel-eslint": "^10.1.0",

src/components/connectAdvanced.tsx

+32-26
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import hoistStatics from 'hoist-non-react-statics'
2-
import React, {
3-
useContext,
4-
useMemo,
5-
useRef,
6-
useReducer,
7-
useLayoutEffect,
8-
} from 'react'
2+
import React, { useContext, useMemo, useRef, useReducer } from 'react'
93
import { isValidElementType, isContextConsumer } from 'react-is'
104
import type { Store } from 'redux'
115
import type { SelectorFactory } from '../connect/selectorFactory'
126
import { createSubscription, Subscription } from '../utils/Subscription'
137
import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
8+
import type { AdvancedComponentDecorator, ConnectedComponent } from '../types'
149

1510
import {
1611
ReactReduxContext,
@@ -31,7 +26,7 @@ const stringifyComponent = (Comp: unknown) => {
3126
}
3227

3328
function storeStateUpdatesReducer(
34-
state: [payload: unknown, counter: number],
29+
state: [unknown, number],
3530
action: { payload: unknown }
3631
) {
3732
const [, updateCount] = state
@@ -108,7 +103,7 @@ function subscribeUpdates(
108103
)
109104
} catch (e) {
110105
error = e
111-
lastThrownError = e
106+
lastThrownError = e as Error | null
112107
}
113108

114109
if (!error) {
@@ -182,16 +177,7 @@ export interface ConnectAdvancedOptions {
182177
pure?: boolean
183178
}
184179

185-
interface AnyObject {
186-
[x: string]: any
187-
}
188-
189-
export default function connectAdvanced<
190-
S,
191-
TProps,
192-
TOwnProps,
193-
TFactoryOptions extends AnyObject = {}
194-
>(
180+
function connectAdvanced<S, TProps, TOwnProps, TFactoryOptions = {}>(
195181
/*
196182
selectorFactory is a func that is responsible for returning the selector function used to
197183
compute new props from state, props, and dispatch. For example:
@@ -235,9 +221,19 @@ export default function connectAdvanced<
235221
) {
236222
const Context = context
237223

238-
return function wrapWithConnect<WC extends React.ComponentType>(
239-
WrappedComponent: WC
240-
) {
224+
type WrappedComponentProps = TOwnProps & ConnectProps
225+
226+
/*
227+
return function wrapWithConnect<
228+
WC extends React.ComponentType<
229+
Matching<DispatchProp<AnyAction>, GetProps<WC>>
230+
>
231+
>(WrappedComponent: WC) {
232+
*/
233+
const wrapWithConnect: AdvancedComponentDecorator<
234+
TProps,
235+
WrappedComponentProps
236+
> = (WrappedComponent) => {
241237
if (
242238
process.env.NODE_ENV !== 'production' &&
243239
!isValidElementType(WrappedComponent)
@@ -483,7 +479,14 @@ export default function connectAdvanced<
483479
// If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
484480
const _Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
485481

486-
const Connect = _Connect as typeof _Connect & { WrappedComponent: WC }
482+
type ConnectedWrapperComponent = typeof _Connect & {
483+
WrappedComponent: typeof WrappedComponent
484+
}
485+
486+
const Connect = _Connect as ConnectedComponent<
487+
typeof WrappedComponent,
488+
WrappedComponentProps
489+
>
487490
Connect.WrappedComponent = WrappedComponent
488491
Connect.displayName = ConnectFunction.displayName = displayName
489492

@@ -492,17 +495,20 @@ export default function connectAdvanced<
492495
props,
493496
ref
494497
) {
498+
// @ts-ignore
495499
return <Connect {...props} reactReduxForwardedRef={ref} />
496500
})
497501

498-
const forwarded = _forwarded as typeof _forwarded & {
499-
WrappedComponent: WC
500-
}
502+
const forwarded = _forwarded as ConnectedWrapperComponent
501503
forwarded.displayName = displayName
502504
forwarded.WrappedComponent = WrappedComponent
503505
return hoistStatics(forwarded, WrappedComponent)
504506
}
505507

506508
return hoistStatics(Connect, WrappedComponent)
507509
}
510+
511+
return wrapWithConnect
508512
}
513+
514+
export default connectAdvanced

0 commit comments

Comments
 (0)