diff --git a/.eslintrc.json b/.eslintrc.json index fd02d43a5..338e924e0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,7 @@ "eslint:recommended", "plugin:import/recommended", "plugin:react/recommended", + "plugin:react/jsx-runtime", "plugin:@typescript-eslint/recommended" ], "rules": { @@ -56,7 +57,7 @@ }, { "files": ["**/test/**/*.{ts,tsx}"], - "parserOptions": { "project": "./test/tsconfig.test.json" } + "parserOptions": { "project": true } } ] } diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b6b807e1..0592ecba2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,15 +1,10 @@ name: Tests -on: - push: - branches: [master] - pull_request: - branches: [master] - workflow_dispatch: +on: [push, pull_request, workflow_dispatch] jobs: build: - name: Test Suite + name: Build and run test Suite runs-on: ubuntu-latest steps: @@ -63,12 +58,24 @@ jobs: node-version: ${{ matrix.node }} cache: 'yarn' + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: package + path: . + - name: Install deps run: yarn install - name: Install TypeScript ${{ matrix.ts }} run: yarn add typescript@${{ matrix.ts }} + - name: Install build artifact + run: yarn add ./package.tgz + + - name: Erase path aliases + run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./vitest.config.mts + - name: Test types run: | yarn tsc --version @@ -219,3 +226,42 @@ jobs: - name: Build example run: yarn build + + test-dist: + name: Run local tests against build artifact + needs: [build] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node: ['20.x'] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Use node ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + + - name: Install deps + run: yarn install + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: package + path: . + + - name: Check folder contents + run: ls -lah + + - name: Install build artifact + run: yarn add ./package.tgz + + - name: Erase path aliases + run: sed -i -e /@remap-prod-remove-line/d ./tsconfig.base.json ./vitest.config.mts + + - name: Run local tests against the build artifact + run: yarn test diff --git a/.gitignore b/.gitignore index b61746065..60f8cecca 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ website/translated_docs website/build/ website/node_modules website/i18n/* + +tsconfig.vitest-temp.json + diff --git a/package.json b/package.json index af3727d03..e37bfba9d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "dist" ], "scripts": { - "build": "tsup", + "build": "yarn clean && tsup", "clean": "rimraf lib dist es coverage", "api-types": "api-extractor run --local", "format": "prettier --write \"{src,test}/**/*.{js,ts,tsx}\" \"docs/**/*.md\"", @@ -44,9 +44,9 @@ "lint:fix": "eslint src test --fix", "prepare": "yarn clean && yarn build", "pretest": "yarn lint", - "test": "vitest --run", + "test": "vitest --run --typecheck", "test:watch": "vitest --watch", - "type-tests": "yarn tsc -p test/typetests/tsconfig.json", + "type-tests": "tsc --noEmit -p tsconfig.test.json", "coverage": "codecov" }, "peerDependencies": { @@ -104,8 +104,8 @@ "redux": "^5.0.0", "rimraf": "^3.0.2", "tsup": "^7.0.0", - "typescript": "^5.4.2", - "vitest": "^1.2.1" + "typescript": "^5.4.5", + "vitest": "^1.6.0" }, "packageManager": "yarn@4.1.0" } diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 65fb669c4..d7f633705 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -91,7 +91,6 @@ function Provider = UnknownAction, S = unknown>({ const Context = context || ReactReduxContext - // @ts-ignore 'AnyAction' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype return {children} } diff --git a/src/components/connect.tsx b/src/components/connect.tsx index a2fc62245..4df0f0bb5 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -519,7 +519,6 @@ function connect< WrappedComponent, // @ts-ignore initMapStateToProps, - // @ts-ignore initMapDispatchToProps, initMergeProps, areStatesEqual, diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index 365f48d9c..c7ced64f5 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -142,7 +142,7 @@ export function createSelectorHook( ? useDefaultReduxContext : createReduxContextHook(context) - const useSelector = ( + const useSelector = ( selector: (state: TState) => Selected, equalityFnOrOptions: | EqualityFn> diff --git a/src/index-rsc.ts b/src/index-rsc.ts index 73ccf50ee..b61fd623e 100644 --- a/src/index-rsc.ts +++ b/src/index-rsc.ts @@ -10,25 +10,25 @@ const _check2: typeof rsc = {} as typeof normal // ------------------------------------------------------------------------------------- -function throwNotSupportedError( +const throwNotSupportedError = (( // eslint-disable-next-line @typescript-eslint/no-unused-vars ...args: any[] -): any { +): any => { throw new Error( 'This function is not supported in React Server Components. Please only use this export in a Client Component.', ) -} +}) as any export { - throwNotSupportedError as batch, throwNotSupportedError as Provider, + throwNotSupportedError as batch, throwNotSupportedError as connect, - throwNotSupportedError as useSelector, - throwNotSupportedError as useDispatch, - throwNotSupportedError as useStore, throwNotSupportedError as createDispatchHook, throwNotSupportedError as createSelectorHook, throwNotSupportedError as createStoreHook, + throwNotSupportedError as useDispatch, + throwNotSupportedError as useSelector, + throwNotSupportedError as useStore, } export const ReactReduxContext = {} as any export { default as shallowEqual } from './utils/shallowEqual' diff --git a/src/utils/react.ts b/src/utils/react.ts index faab8eceb..053b349e6 100644 --- a/src/utils/react.ts +++ b/src/utils/react.ts @@ -1,7 +1,6 @@ -import * as ReactOriginal from 'react' import type * as ReactNamespace from 'react' +import * as ReactOriginal from 'react' export const React: typeof ReactNamespace = // prettier-ignore - // @ts-ignore 'default' in ReactOriginal ? ReactOriginal['default'] : ReactOriginal as any diff --git a/test/components/Provider.spec.tsx b/test/components/Provider.spec.tsx index 1c9dce755..b6b35cab7 100644 --- a/test/components/Provider.spec.tsx +++ b/test/components/Provider.spec.tsx @@ -3,10 +3,10 @@ import * as rtl from '@testing-library/react' import type { Dispatch, JSX } from 'react' import React, { Component } from 'react' +import type { ReactReduxContextValue } from 'react-redux' +import { Provider, ReactReduxContext, connect } from 'react-redux' import type { Store } from 'redux' import { createStore } from 'redux' -import type { ReactReduxContextValue } from '../../src' -import { Provider, ReactReduxContext, connect } from '../../src/index' import * as ReactDOM from 'react-dom' diff --git a/test/components/connect.spec.tsx b/test/components/connect.spec.tsx index 81ff00094..69ba56b2e 100644 --- a/test/components/connect.spec.tsx +++ b/test/components/connect.spec.tsx @@ -1,8 +1,10 @@ /*eslint-disable react/prop-types*/ import * as rtl from '@testing-library/react' -import type { Dispatch, ElementType, MouseEvent, ReactNode, JSX } from 'react' +import type { Dispatch, ElementType, JSX, MouseEvent, ReactNode } from 'react' import React, { Component } from 'react' +import type { ReactReduxContextValue } from 'react-redux' +import { Provider as ProviderMock, connect } from 'react-redux' import type { Action, AnyAction, @@ -12,8 +14,6 @@ import type { UnknownAction, } from 'redux' import { applyMiddleware, createStore } from 'redux' -import type { ReactReduxContextValue } from '../../src/index' -import { Provider as ProviderMock, connect } from '../../src/index' const IS_REACT_18 = React.version.startsWith('18') diff --git a/test/components/hooks.spec.tsx b/test/components/hooks.spec.tsx index a38daaa4c..5872ab148 100644 --- a/test/components/hooks.spec.tsx +++ b/test/components/hooks.spec.tsx @@ -2,9 +2,9 @@ import * as rtl from '@testing-library/react' import React from 'react' +import { Provider as ProviderMock, connect } from 'react-redux' import type { AnyAction } from 'redux' import { createStore } from 'redux' -import { Provider as ProviderMock, connect } from '../../src/index' const IS_REACT_18 = React.version.startsWith('18') diff --git a/test/hooks/hooks.withTypes.test.tsx b/test/hooks/hooks.withTypes.test.tsx index 288a50746..636b65fe2 100644 --- a/test/hooks/hooks.withTypes.test.tsx +++ b/test/hooks/hooks.withTypes.test.tsx @@ -1,6 +1,6 @@ import type { Action, ThunkAction } from '@reduxjs/toolkit' import { configureStore, createAsyncThunk, createSlice } from '@reduxjs/toolkit' -import { useDispatch, useSelector, useStore } from '../../src' +import { useDispatch, useSelector, useStore } from 'react-redux' export interface CounterState { counter: number diff --git a/test/hooks/useDispatch.spec.tsx b/test/hooks/useDispatch.spec.tsx index 5fecddee8..427daa399 100644 --- a/test/hooks/useDispatch.spec.tsx +++ b/test/hooks/useDispatch.spec.tsx @@ -1,12 +1,12 @@ -import React from 'react' -import { createStore } from 'redux' import { renderHook } from '@testing-library/react-hooks' +import React from 'react' +import type { ProviderProps, ReactReduxContextValue } from 'react-redux' import { + createDispatchHook, Provider as ProviderMock, useDispatch, - createDispatchHook, -} from '../../src/index' -import type { ProviderProps, ReactReduxContextValue } from '../../src/' +} from 'react-redux' +import { createStore } from 'redux' const store = createStore((c: number = 1): number => c + 1) const store2 = createStore((c: number = 1): number => c + 2) diff --git a/test/hooks/useReduxContext.spec.tsx b/test/hooks/useReduxContext.spec.tsx index c11740bd2..b06454ecc 100644 --- a/test/hooks/useReduxContext.spec.tsx +++ b/test/hooks/useReduxContext.spec.tsx @@ -1,10 +1,10 @@ -import { renderHook } from '@testing-library/react-hooks' -import { createContext } from 'react' -import type { ReactReduxContextValue } from '../../src/components/Context' import { createReduxContextHook, useReduxContext, -} from '../../src/hooks/useReduxContext' +} from '@internal/hooks/useReduxContext' +import { renderHook } from '@testing-library/react-hooks' +import { createContext } from 'react' +import type { ReactReduxContextValue } from 'react-redux' describe('React', () => { describe('hooks', () => { @@ -14,7 +14,7 @@ describe('React', () => { const { result } = renderHook(() => useReduxContext()) - expect(result.error.message).toMatch( + expect(result.error?.message).toMatch( /could not find react-redux context value/, ) @@ -29,7 +29,7 @@ describe('React', () => { const { result } = renderHook(() => useCustomReduxContext()) - expect(result.error.message).toMatch( + expect(result.error?.message).toMatch( /could not find react-redux context value/, ) diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index e86a522c6..209100916 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -1,5 +1,6 @@ /*eslint-disable react/prop-types*/ +import type { UseSelectorOptions } from '@internal/hooks/useSelector' import * as rtl from '@testing-library/react' import type { DispatchWithoutAction, FunctionComponent, ReactNode } from 'react' import React, { @@ -11,15 +12,12 @@ import React, { useReducer, useState, } from 'react' -import type { Action, AnyAction, Store } from 'redux' -import { createStore } from 'redux' -import type { UseSelectorOptions } from '../../src/hooks/useSelector' import type { ProviderProps, ReactReduxContextValue, Subscription, TypedUseSelectorHook, -} from '../../src/index' +} from 'react-redux' import { Provider, ReactReduxContext, @@ -28,7 +26,9 @@ import { shallowEqual, useDispatch, useSelector, -} from '../../src/index' +} from 'react-redux' +import type { Action, AnyAction, Store } from 'redux' +import { createStore } from 'redux' // disable checks by default function ProviderMock = AnyAction, S = unknown>({ diff --git a/test/integration/dynamic-reducers.spec.tsx b/test/integration/dynamic-reducers.spec.tsx index d199c9748..f2be957a5 100644 --- a/test/integration/dynamic-reducers.spec.tsx +++ b/test/integration/dynamic-reducers.spec.tsx @@ -4,10 +4,10 @@ import * as rtl from '@testing-library/react' import type { ReactNode } from 'react' import React from 'react' import ReactDOMServer from 'react-dom/server' +import type { ReactReduxContextValue } from 'react-redux' +import { Provider, ReactReduxContext, connect } from 'react-redux' import type { Store } from 'redux' import { combineReducers, createStore } from 'redux' -import type { ReactReduxContextValue } from '../../src/index' -import { Provider, ReactReduxContext, connect } from '../../src/index' describe('React', () => { /* @@ -180,10 +180,7 @@ describe('React', () => { // This generates errors for using useLayoutEffect in v7 // We hide that error by disabling console.error here - vi.spyOn(console, 'error') - // eslint-disable-next-line no-console - // @ts-ignore - console.error.mockImplementation(() => {}) + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const markup = ReactDOMServer.renderToString( @@ -199,9 +196,7 @@ describe('React', () => { expect(markup).toContain('Hello world') expect(markup).toContain('Hello dynamic world') - // eslint-disable-next-line no-console - // @ts-ignore - console.error.mockRestore() + consoleErrorSpy.mockRestore() }) }) }) diff --git a/test/integration/server-rendering.spec.tsx b/test/integration/server-rendering.spec.tsx index 9c1e4ccb9..5d88414e3 100644 --- a/test/integration/server-rendering.spec.tsx +++ b/test/integration/server-rendering.spec.tsx @@ -12,9 +12,9 @@ import type { PayloadAction } from '@reduxjs/toolkit' import type { FunctionComponent } from 'react' import React from 'react' import { renderToString } from 'react-dom/server' +import { Provider, connect } from 'react-redux' import type { Dispatch, Store } from 'redux' import { createStore } from 'redux' -import { Provider, connect } from '../../src/index' describe('React', () => { describe('server rendering', () => { diff --git a/test/integration/ssr.spec.tsx b/test/integration/ssr.spec.tsx index 96f638f9a..cb3424874 100644 --- a/test/integration/ssr.spec.tsx +++ b/test/integration/ssr.spec.tsx @@ -4,8 +4,8 @@ import * as rtl from '@testing-library/react' import React, { Suspense, useEffect, useState } from 'react' import { hydrateRoot } from 'react-dom/client' import { renderToString } from 'react-dom/server' -import type { ConnectedProps } from '../../src/index' -import { Provider, connect, useDispatch, useSelector } from '../../src/index' +import type { ConnectedProps } from 'react-redux' +import { Provider, connect, useDispatch, useSelector } from 'react-redux' const IS_REACT_18 = React.version.startsWith('18') diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 000000000..a9d0dd31a --- /dev/null +++ b/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest' diff --git a/test/tsconfig.test.json b/test/tsconfig.test.json deleted file mode 100644 index 461da76db..000000000 --- a/test/tsconfig.test.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "emitDeclarationOnly": false, - "declaration": false, - "strict": true, - "noEmit": true, - "target": "es2018", - "jsx": "react", - "baseUrl": ".", - "skipLibCheck": true, - "noImplicitReturns": false, - "experimentalDecorators": true, - } -} diff --git a/test/typeTestHelpers.ts b/test/typeTestHelpers.ts index 36edf7fb1..4ee6832ad 100644 --- a/test/typeTestHelpers.ts +++ b/test/typeTestHelpers.ts @@ -1,33 +1,3 @@ -/** - * return True if T is `any`, otherwise return False - * taken from https://github.com/joonhocho/tsdef - * - * @internal - */ -export type IsAny = - // test if we are going the left AND right path in the condition - true | false extends (T extends never ? true : false) ? True : False - -/** - * return True if T is `unknown`, otherwise return False - * taken from https://github.com/joonhocho/tsdef - * - * @internal - */ -export type IsUnknown = unknown extends T - ? IsAny - : False - -export function expectType(t: T): T { - return t -} - -type Equals = IsAny< - T, - never, - IsAny -> - export type IsEqual = (() => G extends A ? 1 : 2) extends < G, >() => G extends B ? 1 : 2 @@ -40,46 +10,3 @@ export type IfEquals< TypeIfEquals = unknown, TypeIfNotEquals = never, > = IsEqual extends true ? TypeIfEquals : TypeIfNotEquals - -export declare const exactType: ( - draft: T & IfEquals, - expected: U & IfEquals, -) => IfEquals - -export function expectExactType(t: T) { - return (u: U & Equals) => {} -} - -type EnsureUnknown = IsUnknown -export function expectUnknown>(t: T) { - return t -} - -type EnsureAny = IsAny -export function expectExactAny>(t: T) { - return t -} - -type IsNotAny = IsAny -export function expectNotAny>(t: T): T { - return t -} - -expectType('5' as string) -expectType('5' as const) -expectType('5' as any) -expectExactType('5' as const)('5' as const) -// @ts-expect-error -expectExactType('5' as string)('5' as const) -// @ts-expect-error -expectExactType('5' as any)('5' as const) -expectUnknown('5' as unknown) -// @ts-expect-error -expectUnknown('5' as const) -// @ts-expect-error -expectUnknown('5' as any) -expectExactAny('5' as any) -// @ts-expect-error -expectExactAny('5' as const) -// @ts-expect-error -expectExactAny('5' as unknown) diff --git a/test/typetests/connect-mapstate-mapdispatch.test-d.tsx b/test/typetests/connect-mapstate-mapdispatch.test-d.tsx new file mode 100644 index 000000000..6637c572c --- /dev/null +++ b/test/typetests/connect-mapstate-mapdispatch.test-d.tsx @@ -0,0 +1,495 @@ +import React from 'react' +import type { ActionCreator, Dispatch } from 'redux' +import type { MapDispatchToProps, ReactReduxContextValue } from 'react-redux' +import { connect } from 'react-redux' + +// Test cases written in a way to isolate types and variables and verify the +// output of `connect` to make sure the signature is what is expected + +const CustomContext = React.createContext(null) + +describe('type tests', () => { + test('empty', () => { + interface OwnProps { + dispatch: Dispatch + foo: string + } + + class TestComponent extends React.Component {} + + const Test = connect()(TestComponent) + + const verify = + }) + + test('map state', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + + class TestComponent extends React.Component {} + + const mapStateToProps = (_: any) => ({ + bar: 1, + }) + + const Test = connect(mapStateToProps)(TestComponent) + + const verify = + }) + + test('map state with dispatch prop', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + dispatch: Dispatch + } + + class TestComponent extends React.Component {} + + const mapStateToProps = (_: any) => ({ + bar: 1, + }) + + const Test = connect(mapStateToProps)(TestComponent) + + const verify = + }) + + test('map state factory', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + + class TestComponent extends React.Component {} + + const mapStateToProps = () => () => ({ + bar: 1, + }) + + const Test = connect(mapStateToProps)(TestComponent) + + const verify = + }) + + test('map dispatch', () => { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + const mapDispatchToProps = { onClick: () => {} } + + const TestNull = connect(null, mapDispatchToProps)(TestComponent) + + const verifyNull = + + const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) + + const verifyUndefined = + }) + + test('map dispatch union', () => { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + // We deliberately cast the right-hand side to `any` because otherwise + // TypeScript would maintain the literal value, when we deliberately want to + // test the union type here (as per the annotation). See + // https://github.com/Microsoft/TypeScript/issues/30310#issuecomment-472218182. + const mapDispatchToProps: MapDispatchToProps = + {} as any + + const TestNull = connect(null, mapDispatchToProps)(TestComponent) + + const verifyNull = + + const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) + + const verifyUndefined = + }) + + test('map dispatch with thunk action creators', () => { + const simpleAction = (payload: boolean) => ({ + type: 'SIMPLE_ACTION', + payload, + }) + const thunkAction = + (param1: number, param2: string) => + async (dispatch: Dispatch, { foo }: OwnProps) => { + return foo + } + interface OwnProps { + foo: string + } + interface TestComponentProps extends OwnProps { + simpleAction: typeof simpleAction + thunkAction(param1: number, param2: string): Promise + } + class TestComponent extends React.Component {} + + const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) + const mapDispatchToProps = { simpleAction, thunkAction } + + const Test1 = connect(null, mapDispatchToProps)(TestComponent) + const Test2 = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + const Test3 = connect(null, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const Test4 = connect(mapStateToProps, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const verify = ( +
+ ; + + ; + +
+ ) + }) + + test('map manual dispatch that looks like thunk', () => { + interface OwnProps { + foo: string + } + interface TestComponentProps extends OwnProps { + remove: (item: string) => () => object + } + class TestComponent extends React.Component { + render() { + return
+ } + } + + const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) + function mapDispatchToProps(dispatch: Dispatch) { + return { + remove(item: string) { + return () => dispatch({ type: 'REMOVE_ITEM', item }) + }, + } + } + + const Test1 = connect(null, mapDispatchToProps)(TestComponent) + const Test2 = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + const Test3 = connect(null, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const Test4 = connect(mapStateToProps, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const verify = ( +
+ ; + + ; + +
+ ) + }) + + test('map state and dispatch object', () => { + interface ClickPayload { + count: number + } + const onClick: ActionCreator = () => ({ count: 1 }) + const dispatchToProps = { + onClick, + } + + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: ActionCreator + } + + const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ + bar: 1, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect(mapStateToProps, dispatchToProps)(TestComponent) + + const verify = + }) + + test('map state and nullish dispatch', () => { + interface ClickPayload { + count: number + } + const onClick: ActionCreator = () => ({ count: 1 }) + const dispatchToProps = { + onClick, + } + + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + + const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ + bar: 1, + }) + + class TestComponent extends React.Component {} + + const TestDispatchPropsNull = connect(mapStateToProps, null)(TestComponent) + + const verifyNull = + + const TestDispatchPropsUndefined = connect( + mapStateToProps, + undefined, + )(TestComponent) + + const verifyNonUn = + }) + + test('map dispatch factory', () => { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + const mapDispatchToPropsFactory = () => () => ({ + onClick: () => {}, + }) + + const TestNull = connect(null, mapDispatchToPropsFactory)(TestComponent) + + const verifyNull = + + const TestUndefined = connect( + undefined, + mapDispatchToPropsFactory, + )(TestComponent) + + const verifyUndefined = + }) + + test('map state and dispatch', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const mapStateToProps = () => ({ + bar: 1, + }) + + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) + + const Test = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + + const verify = + }) + + test('map state factory and dispatch', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + const mapStateToPropsFactory = () => () => ({ + bar: 1, + }) + + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect( + mapStateToPropsFactory, + mapDispatchToProps, + )(TestComponent) + + const verify = + }) + + test('map state factory and dispatch factory', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + const mapStateToPropsFactory = () => () => ({ + bar: 1, + }) + + const mapDispatchToPropsFactory = () => () => ({ + onClick: () => {}, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect( + mapStateToPropsFactory, + mapDispatchToPropsFactory, + )(TestComponent) + + const verify = + }) + + test('map state and dispatch and merge', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const mapStateToProps = () => ({ + bar: 1, + }) + + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) + + const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps, + ) => ({ ...stateProps, ...dispatchProps }) + + const Test = connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, + )(TestComponent) + + const verify = + }) + + test('map state and merge', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + const mapStateToProps = () => ({ + bar: 1, + }) + + const mergeProps = ( + stateProps: StateProps, + _: null, + ownProps: OwnProps, + ) => ({ + ...stateProps, + ...ownProps, + }) + + const Test = connect(mapStateToProps, null, mergeProps)(TestComponent) + + const verify = + }) + + test('map state and options', () => { + interface State { + state: string + } + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + dispatch: Dispatch + } + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const mapStateToProps = (state: State) => ({ + bar: 1, + }) + + const areStatePropsEqual = (next: StateProps, current: StateProps) => true + + const Test = connect( + mapStateToProps, + null, + null, + { + areStatePropsEqual, + }, + )(TestComponent) + + const verify = + }) +}) diff --git a/test/typetests/connect-mapstate-mapdispatch.tsx b/test/typetests/connect-mapstate-mapdispatch.tsx deleted file mode 100644 index 8f30cf355..000000000 --- a/test/typetests/connect-mapstate-mapdispatch.tsx +++ /dev/null @@ -1,493 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars, no-inner-declarations */ - -import * as React from 'react' -import type { ActionCreator, Dispatch } from 'redux' -import type { MapDispatchToProps, ReactReduxContext } from '../../src/index' -import { connect } from '../../src/index' - -// Test cases written in a way to isolate types and variables and verify the -// output of `connect` to make sure the signature is what is expected - -const CustomContext = React.createContext( - null, -) as unknown as typeof ReactReduxContext - -function Empty() { - interface OwnProps { - dispatch: Dispatch - foo: string - } - - class TestComponent extends React.Component {} - - const Test = connect()(TestComponent) - - const verify = -} - -function MapState() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - - class TestComponent extends React.Component {} - - const mapStateToProps = (_: any) => ({ - bar: 1, - }) - - const Test = connect(mapStateToProps)(TestComponent) - - const verify = -} - -function MapStateWithDispatchProp() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - dispatch: Dispatch - } - - class TestComponent extends React.Component {} - - const mapStateToProps = (_: any) => ({ - bar: 1, - }) - - const Test = connect(mapStateToProps)(TestComponent) - - const verify = -} - -function MapStateFactory() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - - class TestComponent extends React.Component {} - - const mapStateToProps = () => () => ({ - bar: 1, - }) - - const Test = connect(mapStateToProps)(TestComponent) - - const verify = -} - -function MapDispatch() { - interface OwnProps { - foo: string - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component {} - - const mapDispatchToProps = { onClick: () => {} } - - const TestNull = connect(null, mapDispatchToProps)(TestComponent) - - const verifyNull = - - const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) - - const verifyUndefined = -} - -function MapDispatchUnion() { - interface OwnProps { - foo: string - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component {} - - // We deliberately cast the right-hand side to `any` because otherwise - // TypeScript would maintain the literal value, when we deliberately want to - // test the union type here (as per the annotation). See - // https://github.com/Microsoft/TypeScript/issues/30310#issuecomment-472218182. - const mapDispatchToProps: MapDispatchToProps = - {} as any - - const TestNull = connect(null, mapDispatchToProps)(TestComponent) - - const verifyNull = - - const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) - - const verifyUndefined = -} - -function MapDispatchWithThunkActionCreators() { - const simpleAction = (payload: boolean) => ({ - type: 'SIMPLE_ACTION', - payload, - }) - const thunkAction = - (param1: number, param2: string) => - async (dispatch: Dispatch, { foo }: OwnProps) => { - return foo - } - interface OwnProps { - foo: string - } - interface TestComponentProps extends OwnProps { - simpleAction: typeof simpleAction - thunkAction(param1: number, param2: string): Promise - } - class TestComponent extends React.Component {} - - const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) - const mapDispatchToProps = { simpleAction, thunkAction } - - const Test1 = connect(null, mapDispatchToProps)(TestComponent) - const Test2 = connect(mapStateToProps, mapDispatchToProps)(TestComponent) - const Test3 = connect(null, mapDispatchToProps, null, { - context: CustomContext, - })(TestComponent) - const Test4 = connect(mapStateToProps, mapDispatchToProps, null, { - context: CustomContext, - })(TestComponent) - const verify = ( -
- ; - - ; - -
- ) -} - -function MapManualDispatchThatLooksLikeThunk() { - interface OwnProps { - foo: string - } - interface TestComponentProps extends OwnProps { - remove: (item: string) => () => object - } - class TestComponent extends React.Component { - render() { - return
- } - } - - const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) - function mapDispatchToProps(dispatch: Dispatch) { - return { - remove(item: string) { - return () => dispatch({ type: 'REMOVE_ITEM', item }) - }, - } - } - - const Test1 = connect(null, mapDispatchToProps)(TestComponent) - const Test2 = connect(mapStateToProps, mapDispatchToProps)(TestComponent) - const Test3 = connect(null, mapDispatchToProps, null, { - context: CustomContext, - })(TestComponent) - const Test4 = connect(mapStateToProps, mapDispatchToProps, null, { - context: CustomContext, - })(TestComponent) - const verify = ( -
- ; - - ; - -
- ) -} - -function MapStateAndDispatchObject() { - interface ClickPayload { - count: number - } - const onClick: ActionCreator = () => ({ count: 1 }) - const dispatchToProps = { - onClick, - } - - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: ActionCreator - } - - const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ - bar: 1, - }) - - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} - - const Test = connect(mapStateToProps, dispatchToProps)(TestComponent) - - const verify = -} - -function MapStateAndNullishDispatch() { - interface ClickPayload { - count: number - } - const onClick: ActionCreator = () => ({ count: 1 }) - const dispatchToProps = { - onClick, - } - - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - - const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ - bar: 1, - }) - - class TestComponent extends React.Component {} - - const TestDispatchPropsNull = connect(mapStateToProps, null)(TestComponent) - - const verifyNull = - - const TestDispatchPropsUndefined = connect( - mapStateToProps, - undefined, - )(TestComponent) - - const verifyNonUn = -} - -function MapDispatchFactory() { - interface OwnProps { - foo: string - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component {} - - const mapDispatchToPropsFactory = () => () => ({ - onClick: () => {}, - }) - - const TestNull = connect(null, mapDispatchToPropsFactory)(TestComponent) - - const verifyNull = - - const TestUndefined = connect( - undefined, - mapDispatchToPropsFactory, - )(TestComponent) - - const verifyUndefined = -} - -function MapStateAndDispatch() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} - - const mapStateToProps = () => ({ - bar: 1, - }) - - const mapDispatchToProps = () => ({ - onClick: () => {}, - }) - - const Test = connect(mapStateToProps, mapDispatchToProps)(TestComponent) - - const verify = -} - -function MapStateFactoryAndDispatch() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } - - const mapStateToPropsFactory = () => () => ({ - bar: 1, - }) - - const mapDispatchToProps = () => ({ - onClick: () => {}, - }) - - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} - - const Test = connect( - mapStateToPropsFactory, - mapDispatchToProps, - )(TestComponent) - - const verify = -} - -function MapStateFactoryAndDispatchFactory() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } - - const mapStateToPropsFactory = () => () => ({ - bar: 1, - }) - - const mapDispatchToPropsFactory = () => () => ({ - onClick: () => {}, - }) - - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} - - const Test = connect( - mapStateToPropsFactory, - mapDispatchToPropsFactory, - )(TestComponent) - - const verify = -} - -function MapStateAndDispatchAndMerge() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} - - const mapStateToProps = () => ({ - bar: 1, - }) - - const mapDispatchToProps = () => ({ - onClick: () => {}, - }) - - const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ) => ({ ...stateProps, ...dispatchProps }) - - const Test = connect( - mapStateToProps, - mapDispatchToProps, - mergeProps, - )(TestComponent) - - const verify = -} - -function MapStateAndMerge() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component {} - - const mapStateToProps = () => ({ - bar: 1, - }) - - const mergeProps = (stateProps: StateProps, _: null, ownProps: OwnProps) => ({ - ...stateProps, - ...ownProps, - }) - - const Test = connect(mapStateToProps, null, mergeProps)(TestComponent) - - const verify = -} - -function MapStateAndOptions() { - interface State { - state: string - } - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - dispatch: Dispatch - } - - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} - - const mapStateToProps = (state: State) => ({ - bar: 1, - }) - - const areStatePropsEqual = (next: StateProps, current: StateProps) => true - - const Test = connect( - mapStateToProps, - null, - null, - { - areStatePropsEqual, - }, - )(TestComponent) - - const verify = -} diff --git a/test/typetests/connect-options-and-issues.test-d.tsx b/test/typetests/connect-options-and-issues.test-d.tsx new file mode 100644 index 000000000..52f1c67bd --- /dev/null +++ b/test/typetests/connect-options-and-issues.test-d.tsx @@ -0,0 +1,901 @@ +import PropTypes from 'prop-types' +import React from 'react' +import type { + Connect, + ConnectedProps, + DispatchProp, + MapStateToProps, + ReactReduxContextValue, +} from 'react-redux' +import { Provider, ReactReduxContext, connect } from 'react-redux' +import type { ActionCreator, AnyAction, Dispatch, Reducer, Store } from 'redux' +import { createStore } from 'redux' + +// Test cases written in a way to isolate types and variables and verify the +// output of `connect` to make sure the signature is what is expected + +const CustomContext = React.createContext(null) + +describe('type tests', () => { + test('merged props inference', () => { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16021 + + interface StateProps { + state: string + } + + interface DispatchProps { + dispatch: string + } + + interface OwnProps { + own: string + } + + interface MergedProps { + merged: string + } + + class MergedPropsComponent extends React.Component { + render() { + return
+ } + } + + function mapStateToProps(state: any): StateProps { + return { state: 'string' } + } + + function mapDispatchToProps(dispatch: Dispatch): DispatchProps { + return { dispatch: 'string' } + } + + const ConnectedWithOwnAndState = connect< + StateProps, + void, + OwnProps, + MergedProps + >(mapStateToProps, undefined, (stateProps: StateProps) => ({ + merged: 'merged', + }))(MergedPropsComponent) + + const ConnectedWithOwnAndDispatch = connect< + void, + DispatchProps, + OwnProps, + MergedProps + >( + undefined, + mapDispatchToProps, + (stateProps: undefined, dispatchProps: DispatchProps) => ({ + merged: 'merged', + }), + )(MergedPropsComponent) + + const ConnectedWithOwn = connect( + undefined, + undefined, + () => ({ + merged: 'merged', + }), + )(MergedPropsComponent) + + const ConnectedWithInferredDispatch = connect( + mapStateToProps, + undefined, + (stateProps, dispatchProps, ownProps) => { + expectTypeOf(dispatchProps).toEqualTypeOf>() + }, + )(MergedPropsComponent) + }) + + test('issue #16652: expose dispatch with props', () => { + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/16652 + + interface PassedProps { + commentIds: string[] + } + + interface GeneratedStateProps { + comments: Array<{ id: string } | undefined> + } + + class CommentList extends React.Component< + PassedProps & GeneratedStateProps & DispatchProp + > {} + + const mapStateToProps = ( + state: any, + ownProps: PassedProps, + ): GeneratedStateProps => { + return { + comments: ownProps.commentIds.map((id) => ({ id })), + } + } + + const ConnectedCommentList = connect( + mapStateToProps, + )(CommentList) + + ; + }) + + test('issue #15463', () => { + interface SpinnerProps { + showGlobalSpinner: boolean + } + + class SpinnerClass extends React.Component { + render() { + return
+ } + } + + const Spinner = connect((state: any) => { + return { showGlobalSpinner: true } + })(SpinnerClass) + + ; + }) + + test('remove injected and pass on rest', () => { + interface TProps { + showGlobalSpinner: boolean + foo: string + } + class SpinnerClass extends React.Component { + render() { + return
+ } + } + + const Spinner = connect((state: any) => { + return { showGlobalSpinner: true } + })(SpinnerClass) + + ; + }) + + test('controlled component without DispatchProp', () => { + interface MyState { + count: number + } + + interface MyProps { + label: string + // `dispatch` is optional, but setting it to anything + // other than Dispatch will cause an error + // + // dispatch: Dispatch; // OK + // dispatch: number; // ERROR + } + + function mapStateToProps(state: MyState) { + return { + label: `The count is ${state.count}`, + } + } + + class MyComponent extends React.Component { + render() { + return {this.props.label} + } + } + + const MyFuncComponent = (props: MyProps) => {props.label} + + const MyControlledComponent = connect(mapStateToProps)(MyComponent) + const MyControlledFuncComponent = connect(mapStateToProps)(MyFuncComponent) + }) + + test('dispatch to props as object', () => { + const onClick: ActionCreator<{}> = () => ({}) + const mapStateToProps = (state: any) => { + return { + title: state.app.title as string, + } + } + const dispatchToProps = { + onClick, + } + + type Props = { title: string } & typeof dispatchToProps + const HeaderComponent: React.FunctionComponent = (props) => { + return

{props.title}

+ } + + const Header = connect(mapStateToProps, dispatchToProps)(HeaderComponent) + ;
+ }) + + test('inferred functional component with explicit own props', () => { + interface Props { + title: string + extraText: string + onClick: () => void + } + + const Header = connect( + ( + { app: { title } }: { app: { title: string } }, + { extraText }: { extraText: string }, + ) => ({ + title, + extraText, + }), + (dispatch) => ({ + onClick: () => dispatch({ type: 'test' }), + }), + )(({ title, extraText, onClick }: Props) => { + return ( +

+ {title} {extraText} +

+ ) + }) + ;
+ }) + + test('inferred functional component with implicit own props', () => { + interface Props { + title: string + extraText: string + onClick: () => void + } + + const Header = connect( + ({ app: { title } }: { app: { title: string } }) => ({ + title, + }), + (dispatch) => ({ + onClick: () => dispatch({ type: 'test' }), + }), + )(({ title, extraText, onClick }: Props) => { + return ( +

+ {title} {extraText} +

+ ) + }) + ;
+ }) + + test('wrapped component', () => { + interface InnerProps { + name: string + } + const Inner: React.FunctionComponent = (props) => { + return

{props.name}

+ } + + const mapStateToProps = (state: any) => { + return { + name: 'Connected', + } + } + const Connected = connect(mapStateToProps)(Inner) + + // `Inner` and `Connected.WrappedComponent` require explicit `name` prop + const TestInner = (props: any) => + const TestWrapped = (props: any) => ( + + ) + // `Connected` does not require explicit `name` prop + const TestConnected = (props: any) => + }) + + test('without own props decorated inference', () => { + interface ForwardedProps { + forwarded: string + } + + interface OwnProps { + own: string + } + + interface StateProps { + state: string + } + + class WithoutOwnPropsComponentClass extends React.Component< + ForwardedProps & StateProps & DispatchProp + > { + render() { + return
+ } + } + + const WithoutOwnPropsComponentStateless: React.FunctionComponent< + ForwardedProps & StateProps & DispatchProp + > = () =>
+ + function mapStateToProps4(state: any, ownProps: OwnProps): StateProps { + return { state: 'string' } + } + + // these decorations should compile, it is perfectly acceptable to receive props and ignore them + const ConnectedWithOwnPropsClass = connect(mapStateToProps4)( + WithoutOwnPropsComponentClass, + ) + const ConnectedWithOwnPropsStateless = connect(mapStateToProps4)( + WithoutOwnPropsComponentStateless, + ) + const ConnectedWithTypeHintClass = connect( + mapStateToProps4, + )(WithoutOwnPropsComponentClass) + const ConnectedWithTypeHintStateless = connect( + mapStateToProps4, + )(WithoutOwnPropsComponentStateless) + + // This should compile + React.createElement(ConnectedWithOwnPropsClass, { + own: 'string', + forwarded: 'string', + }) + React.createElement(ConnectedWithOwnPropsClass, { + own: 'string', + forwarded: 'string', + }) + + // This should not compile, it is missing ForwardedProps + // @ts-expect-error + React.createElement(ConnectedWithOwnPropsClass, { own: 'string' }) + // @ts-expect-error + React.createElement(ConnectedWithOwnPropsStateless, { own: 'string' }) + + // This should compile + React.createElement(ConnectedWithOwnPropsClass, { + own: 'string', + forwarded: 'string', + }) + React.createElement(ConnectedWithOwnPropsStateless, { + own: 'string', + forwarded: 'string', + }) + + // This should not compile, it is missing ForwardedProps + // @ts-expect-error + React.createElement(ConnectedWithTypeHintClass, { own: 'string' }) + // @ts-expect-error + React.createElement(ConnectedWithTypeHintStateless, { own: 'string' }) + + interface AllProps { + own: string + state: string + } + + class AllPropsComponent extends React.Component< + AllProps & DispatchProp + > { + render() { + return
+ } + } + + type PickedOwnProps = Pick + type PickedStateProps = Pick + + const mapStateToPropsForPicked: MapStateToProps< + PickedStateProps, + PickedOwnProps, + {} + > = (state: any): PickedStateProps => { + return { state: 'string' } + } + const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)( + AllPropsComponent, + ) + ; + }) + + test('provider accepts store with custom action', () => { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25321#issuecomment-387659500 + + const reducer: Reducer< + { foo: number } | undefined, + { type: 'foo'; payload: number } + > = (state) => state + + const store = createStore(reducer) + + const Whatever = () => ( + +
Whatever
+
+ ) + }) + + test('optional props merged correctly', () => { + interface OptionalDecorationProps { + foo: string + bar: number + optionalProp?: boolean | undefined + dependsOnDispatch?: (() => void) | undefined + } + + class Component extends React.Component { + render() { + return
+ } + } + + interface MapStateProps { + foo: string + bar: number + optionalProp: boolean + } + + interface MapDispatchProps { + dependsOnDispatch: () => void + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + optionalProp: true, + } + } + + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } + + const Connected = connect(mapStateToProps, mapDispatchToProps)(Component) + }) + + test('more general decoration props', () => { + // connect() should support decoration props that are more permissive + // than the injected props, as long as the injected props can satisfy + // the decoration props. + interface MoreGeneralDecorationProps { + foo: string | number + bar: number | 'foo' + optionalProp?: boolean | object | undefined + dependsOnDispatch?: (() => void) | undefined + } + + class Component extends React.Component { + render() { + return
+ } + } + + interface MapStateProps { + foo: string + bar: number + optionalProp: boolean + } + + interface MapDispatchProps { + dependsOnDispatch: () => void + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + optionalProp: true, + } + } + + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } + + connect(mapStateToProps, mapDispatchToProps)(Component) + }) + + test('fails more specific injected props', () => { + interface MoreSpecificDecorationProps { + foo: string + bar: number + dependsOnDispatch: () => void + } + + class Component extends React.Component { + render() { + return
+ } + } + + interface MapStateProps { + foo: string | number + bar: number | 'foo' + dependsOnDispatch?: (() => void) | undefined + } + + interface MapDispatchProps { + dependsOnDispatch?: (() => void) | undefined + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + } + } + + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } + + // Since it is possible the injected props could fail to satisfy the decoration props, + // the following line should fail to compile. + // @ts-expect-error + connect(mapStateToProps, mapDispatchToProps)(Component) + + // Confirm that this also fails with functional components + const FunctionalComponent = (props: MoreSpecificDecorationProps) => null + // @ts-expect-error + connect(mapStateToProps, mapDispatchToProps)(Component) + }) + + test('library managed attributes', () => { + interface OwnProps { + bar: number + fn: () => void + } + + interface ExternalOwnProps { + bar?: number | undefined + fn: () => void + } + + interface MapStateProps { + foo: string + } + + class Component extends React.Component { + static defaultProps = { + bar: 0, + } + + render() { + return
+ } + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + } + } + + const ConnectedComponent = connect(mapStateToProps)(Component) + ; {}} /> + + const ConnectedComponent2 = connect( + mapStateToProps, + )(Component) + ; {}} /> + }) + + test('PropTypes', () => { + interface OwnProps { + bar: number + fn: () => void + } + + interface MapStateProps { + foo: string + } + + class Component extends React.Component { + static propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number.isRequired, + fn: PropTypes.func.isRequired, + } + + render() { + return
+ } + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + } + } + + const ConnectedComponent = connect(mapStateToProps)(Component) + ; {}} bar={0} /> + + const ConnectedComponent2 = connect( + mapStateToProps, + )(Component) + ; {}} bar={0} /> + }) + + test('non react statics', () => { + interface OwnProps { + bar: number + } + + interface MapStateProps { + foo: string + } + + class Component extends React.Component { + static defaultProps = { + bar: 0, + } + + static meaningOfLife = 42 + + render() { + return
+ } + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + } + } + + Component.meaningOfLife + Component.defaultProps.bar + + const ConnectedComponent = connect(mapStateToProps)(Component) + + // This is a non-React static and should be hoisted as-is. + ConnectedComponent.meaningOfLife + + // This is a React static, so it's not hoisted. + // However, ConnectedComponent is still a ComponentClass, which specifies `defaultProps` + // as an optional static member. We can force an error (and assert that `defaultProps` + // wasn't hoisted) by reaching into the `defaultProps` object without a null check. + // @ts-expect-error + ConnectedComponent.defaultProps.bar + }) + + test('Provider Context', () => { + const store: Store = createStore((state = {}) => state) + const nullContext = React.createContext(null) + + // To ensure type safety when consuming the context in an app, a null-context does not suffice. + // @ts-expect-error + ; + ; +
+ + + // react-redux exports a default context used internally if none is supplied, used as shown below. + class ComponentWithDefaultContext extends React.Component { + static contextType = ReactReduxContext + } + + // eslint-disable-next-line no-extra-semi + ; + + + + // Null is not a valid value for the context. + // @ts-expect-error + ; + }) + + test('connected props', () => { + interface OwnProps { + own: string + } + const Component: React.FC = ({ own, dispatch }) => + null + + const connector = connect() + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) + }) + + test('connected props with state', () => { + interface OwnProps { + own: string + } + const Component: React.FC = ({ + own, + injected, + dispatch, + }) => { + injected.slice() + return null + } + + const connector = connect((state: any) => ({ injected: '' })) + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) + }) + + test('connected props with state and actions', () => { + interface OwnProps { + own: string + } + const actionCreator = () => ({ type: 'action' }) + + const Component: React.FC = ({ + own, + injected, + actionCreator, + }) => { + actionCreator() + return null + } + + const ComponentWithDispatch: React.FC = ({ + own, + // @ts-expect-error + dispatch, + }) => null + + const connector = connect((state: any) => ({ injected: '' }), { + actionCreator, + }) + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) + }) + + test('connect return type', () => { + const TestComponent: React.FC = () => null + + const Test = connect()(TestComponent) + + const myHoc1 = (C: React.ComponentClass

): React.ComponentType

=> C + // @ts-expect-error + myHoc1(Test) + + const myHoc2 = (C: React.FC

): React.ComponentType

=> C + // TODO Figure out the error here + // myHoc2(Test) + }) + + test('Ref', () => { + const FunctionalComponent: React.FC = () => null + const ForwardedFunctionalComponent = React.forwardRef(() => null) + class ClassComponent extends React.Component {} + + const ConnectedFunctionalComponent = connect()(FunctionalComponent) + const ConnectedForwardedFunctionalComponent = connect()( + ForwardedFunctionalComponent, + ) + const ConnectedClassComponent = connect()(ClassComponent) + + // Should not be able to pass any type of ref to a FunctionalComponent + // ref is not a valid property + ;()} + /> + ; {}} + /> + + // @ts-expect-error + ; + + // Should be able to pass modern refs to a ForwardRefExoticComponent + const modernRef: React.Ref | undefined = undefined + ; + // Should not be able to use legacy string refs + ; + // ref type should agree with type of the forwarded ref + ;()} + /> + ; {}} + /> + + // Should be able to use all refs (except legacy string refs, which go away in React 19) + const classLegacyRef: React.Ref | undefined = undefined + ; + ;()} /> + ; {}} /> + // TODO Can make this an expected error if we target React 19 exclusively + // ; + ;()} + /> + ; {}} + /> + }) + + test('connect default state', () => { + connect((state) => { + const s = state + + expectTypeOf(s).toBeUnknown() + + return state + }) + + const connectWithDefaultState: Connect<{ value: number }> = connect + connectWithDefaultState((state) => { + expectTypeOf(state).toEqualTypeOf<{ value: number }>() + + return state + }) + }) + + test('preserve discriminated unions', () => { + type OwnPropsT = { + color: string + } & ( + | { + type: 'plain' + } + | { + type: 'localized' + params: Record | undefined + } + ) + + class MyText extends React.Component {} + + const ConnectedMyText = connect()(MyText) + const someParams = { key: 'value', foo: 'bar' } + + ; + // @ts-expect-error + ; + // @ts-expect-error + ; + ; + }) + + test('issue #1187 connect accepts prop named context', () => { + // https://github.com/reduxjs/react-redux/issues/1187 + + 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 + }) +}) diff --git a/test/typetests/connect-options-and-issues.tsx b/test/typetests/connect-options-and-issues.tsx deleted file mode 100644 index 1579397c4..000000000 --- a/test/typetests/connect-options-and-issues.tsx +++ /dev/null @@ -1,899 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars, react/prop-types */ -import * as PropTypes from 'prop-types' -import * as React from 'react' -import type { ActionCreator, AnyAction, Dispatch, Reducer, Store } from 'redux' -import { createStore } from 'redux' -import type { - Connect, - ConnectedProps, - DispatchProp, - MapStateToProps, -} from '../../src/index' -import { Provider, ReactReduxContext, connect } from '../../src/index' - -import { expectType } from '../typeTestHelpers' - -// Test cases written in a way to isolate types and variables and verify the -// output of `connect` to make sure the signature is what is expected - -const CustomContext = React.createContext( - null, -) as unknown as typeof ReactReduxContext - -// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16021 -function TestMergedPropsInference() { - interface StateProps { - state: string - } - - interface DispatchProps { - dispatch: string - } - - interface OwnProps { - own: string - } - - interface MergedProps { - merged: string - } - - class MergedPropsComponent extends React.Component { - render() { - return

- } - } - - function mapStateToProps(state: any): StateProps { - return { state: 'string' } - } - - function mapDispatchToProps(dispatch: Dispatch): DispatchProps { - return { dispatch: 'string' } - } - - const ConnectedWithOwnAndState = connect< - StateProps, - void, - OwnProps, - MergedProps - >(mapStateToProps, undefined, (stateProps: StateProps) => ({ - merged: 'merged', - }))(MergedPropsComponent) - - const ConnectedWithOwnAndDispatch = connect< - void, - DispatchProps, - OwnProps, - MergedProps - >( - undefined, - mapDispatchToProps, - (stateProps: undefined, dispatchProps: DispatchProps) => ({ - merged: 'merged', - }), - )(MergedPropsComponent) - - const ConnectedWithOwn = connect( - undefined, - undefined, - () => ({ - merged: 'merged', - }), - )(MergedPropsComponent) - - const ConnectedWithInferredDispatch = connect( - mapStateToProps, - undefined, - (stateProps, dispatchProps, ownProps) => { - expectType>(dispatchProps) - }, - )(MergedPropsComponent) -} - -function Issue16652() { - interface PassedProps { - commentIds: string[] - } - - interface GeneratedStateProps { - comments: Array<{ id: string } | undefined> - } - - class CommentList extends React.Component< - PassedProps & GeneratedStateProps & DispatchProp - > {} - - const mapStateToProps = ( - state: any, - ownProps: PassedProps, - ): GeneratedStateProps => { - return { - comments: ownProps.commentIds.map((id) => ({ id })), - } - } - - const ConnectedCommentList = connect( - mapStateToProps, - )(CommentList) - - ; -} - -function Issue15463() { - interface SpinnerProps { - showGlobalSpinner: boolean - } - - class SpinnerClass extends React.Component { - render() { - return
- } - } - - const Spinner = connect((state: any) => { - return { showGlobalSpinner: true } - })(SpinnerClass) - - ; -} - -function RemoveInjectedAndPassOnRest() { - interface TProps { - showGlobalSpinner: boolean - foo: string - } - class SpinnerClass extends React.Component { - render() { - return
- } - } - - const Spinner = connect((state: any) => { - return { showGlobalSpinner: true } - })(SpinnerClass) - - ; -} - -function TestControlledComponentWithoutDispatchProp() { - interface MyState { - count: number - } - - interface MyProps { - label: string - // `dispatch` is optional, but setting it to anything - // other than Dispatch will cause an error - // - // dispatch: Dispatch; // OK - // dispatch: number; // ERROR - } - - function mapStateToProps(state: MyState) { - return { - label: `The count is ${state.count}`, - } - } - - class MyComponent extends React.Component { - render() { - return {this.props.label} - } - } - - const MyFuncComponent = (props: MyProps) => {props.label} - - const MyControlledComponent = connect(mapStateToProps)(MyComponent) - const MyControlledFuncComponent = connect(mapStateToProps)(MyFuncComponent) -} - -function TestDispatchToPropsAsObject() { - const onClick: ActionCreator<{}> = () => ({}) - const mapStateToProps = (state: any) => { - return { - title: state.app.title as string, - } - } - const dispatchToProps = { - onClick, - } - - type Props = { title: string } & typeof dispatchToProps - const HeaderComponent: React.FunctionComponent = (props) => { - return

{props.title}

- } - - const Header = connect(mapStateToProps, dispatchToProps)(HeaderComponent) - ;
-} - -function TestInferredFunctionalComponentWithExplicitOwnProps() { - interface Props { - title: string - extraText: string - onClick: () => void - } - - const Header = connect( - ( - { app: { title } }: { app: { title: string } }, - { extraText }: { extraText: string }, - ) => ({ - title, - extraText, - }), - (dispatch) => ({ - onClick: () => dispatch({ type: 'test' }), - }), - )(({ title, extraText, onClick }: Props) => { - return ( -

- {title} {extraText} -

- ) - }) - ;
-} - -function TestInferredFunctionalComponentWithImplicitOwnProps() { - interface Props { - title: string - extraText: string - onClick: () => void - } - - const Header = connect( - ({ app: { title } }: { app: { title: string } }) => ({ - title, - }), - (dispatch) => ({ - onClick: () => dispatch({ type: 'test' }), - }), - )(({ title, extraText, onClick }: Props) => { - return ( -

- {title} {extraText} -

- ) - }) - ;
-} - -function TestWrappedComponent() { - interface InnerProps { - name: string - } - const Inner: React.FunctionComponent = (props) => { - return

{props.name}

- } - - const mapStateToProps = (state: any) => { - return { - name: 'Connected', - } - } - const Connected = connect(mapStateToProps)(Inner) - - // `Inner` and `Connected.WrappedComponent` require explicit `name` prop - const TestInner = (props: any) => - const TestWrapped = (props: any) => ( - - ) - // `Connected` does not require explicit `name` prop - const TestConnected = (props: any) => -} - -function TestWithoutTOwnPropsDecoratedInference() { - interface ForwardedProps { - forwarded: string - } - - interface OwnProps { - own: string - } - - interface StateProps { - state: string - } - - class WithoutOwnPropsComponentClass extends React.Component< - ForwardedProps & StateProps & DispatchProp - > { - render() { - return
- } - } - - const WithoutOwnPropsComponentStateless: React.FunctionComponent< - ForwardedProps & StateProps & DispatchProp - > = () =>
- - function mapStateToProps4(state: any, ownProps: OwnProps): StateProps { - return { state: 'string' } - } - - // these decorations should compile, it is perfectly acceptable to receive props and ignore them - const ConnectedWithOwnPropsClass = connect(mapStateToProps4)( - WithoutOwnPropsComponentClass, - ) - const ConnectedWithOwnPropsStateless = connect(mapStateToProps4)( - WithoutOwnPropsComponentStateless, - ) - const ConnectedWithTypeHintClass = connect( - mapStateToProps4, - )(WithoutOwnPropsComponentClass) - const ConnectedWithTypeHintStateless = connect( - mapStateToProps4, - )(WithoutOwnPropsComponentStateless) - - // This should compile - React.createElement(ConnectedWithOwnPropsClass, { - own: 'string', - forwarded: 'string', - }) - React.createElement(ConnectedWithOwnPropsClass, { - own: 'string', - forwarded: 'string', - }) - - // This should not compile, it is missing ForwardedProps - // @ts-expect-error - React.createElement(ConnectedWithOwnPropsClass, { own: 'string' }) - // @ts-expect-error - React.createElement(ConnectedWithOwnPropsStateless, { own: 'string' }) - - // This should compile - React.createElement(ConnectedWithOwnPropsClass, { - own: 'string', - forwarded: 'string', - }) - React.createElement(ConnectedWithOwnPropsStateless, { - own: 'string', - forwarded: 'string', - }) - - // This should not compile, it is missing ForwardedProps - // @ts-expect-error - React.createElement(ConnectedWithTypeHintClass, { own: 'string' }) - // @ts-expect-error - React.createElement(ConnectedWithTypeHintStateless, { own: 'string' }) - - interface AllProps { - own: string - state: string - } - - class AllPropsComponent extends React.Component< - AllProps & DispatchProp - > { - render() { - return
- } - } - - type PickedOwnProps = Pick - type PickedStateProps = Pick - - const mapStateToPropsForPicked: MapStateToProps< - PickedStateProps, - PickedOwnProps, - {} - > = (state: any): PickedStateProps => { - return { state: 'string' } - } - const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)( - AllPropsComponent, - ) - ; -} - -// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25321#issuecomment-387659500 -function ProviderAcceptsStoreWithCustomAction() { - const reducer: Reducer< - { foo: number } | undefined, - { type: 'foo'; payload: number } - > = (state) => state - - const store = createStore(reducer) - - const Whatever = () => ( - -
Whatever
-
- ) -} - -function TestOptionalPropsMergedCorrectly() { - interface OptionalDecorationProps { - foo: string - bar: number - optionalProp?: boolean | undefined - dependsOnDispatch?: (() => void) | undefined - } - - class Component extends React.Component { - render() { - return
- } - } - - interface MapStateProps { - foo: string - bar: number - optionalProp: boolean - } - - interface MapDispatchProps { - dependsOnDispatch: () => void - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - bar: 42, - optionalProp: true, - } - } - - function mapDispatchToProps(dispatch: any): MapDispatchProps { - return { - dependsOnDispatch: () => {}, - } - } - - const Connected = connect(mapStateToProps, mapDispatchToProps)(Component) -} - -function TestMoreGeneralDecorationProps() { - // connect() should support decoration props that are more permissive - // than the injected props, as long as the injected props can satisfy - // the decoration props. - interface MoreGeneralDecorationProps { - foo: string | number - bar: number | 'foo' - optionalProp?: boolean | object | undefined - dependsOnDispatch?: (() => void) | undefined - } - - class Component extends React.Component { - render() { - return
- } - } - - interface MapStateProps { - foo: string - bar: number - optionalProp: boolean - } - - interface MapDispatchProps { - dependsOnDispatch: () => void - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - bar: 42, - optionalProp: true, - } - } - - function mapDispatchToProps(dispatch: any): MapDispatchProps { - return { - dependsOnDispatch: () => {}, - } - } - - connect(mapStateToProps, mapDispatchToProps)(Component) -} - -function TestFailsMoreSpecificInjectedProps() { - interface MoreSpecificDecorationProps { - foo: string - bar: number - dependsOnDispatch: () => void - } - - class Component extends React.Component { - render() { - return
- } - } - - interface MapStateProps { - foo: string | number - bar: number | 'foo' - dependsOnDispatch?: (() => void) | undefined - } - - interface MapDispatchProps { - dependsOnDispatch?: (() => void) | undefined - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - bar: 42, - } - } - - function mapDispatchToProps(dispatch: any): MapDispatchProps { - return { - dependsOnDispatch: () => {}, - } - } - - // Since it is possible the injected props could fail to satisfy the decoration props, - // the following line should fail to compile. - // @ts-expect-error - connect(mapStateToProps, mapDispatchToProps)(Component) - - // Confirm that this also fails with functional components - const FunctionalComponent = (props: MoreSpecificDecorationProps) => null - // @ts-expect-error - connect(mapStateToProps, mapDispatchToProps)(Component) -} - -function TestLibraryManagedAttributes() { - interface OwnProps { - bar: number - fn: () => void - } - - interface ExternalOwnProps { - bar?: number | undefined - fn: () => void - } - - interface MapStateProps { - foo: string - } - - class Component extends React.Component { - static defaultProps = { - bar: 0, - } - - render() { - return
- } - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - } - } - - const ConnectedComponent = connect(mapStateToProps)(Component) - ; {}} /> - - const ConnectedComponent2 = connect( - mapStateToProps, - )(Component) - ; {}} /> -} - -function TestPropTypes() { - interface OwnProps { - bar: number - fn: () => void - } - - interface MapStateProps { - foo: string - } - - class Component extends React.Component { - static propTypes = { - foo: PropTypes.string.isRequired, - bar: PropTypes.number.isRequired, - fn: PropTypes.func.isRequired, - } - - render() { - return
- } - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - } - } - - const ConnectedComponent = connect(mapStateToProps)(Component) - ; {}} bar={0} /> - - const ConnectedComponent2 = connect( - mapStateToProps, - )(Component) - ; {}} bar={0} /> -} - -function TestNonReactStatics() { - interface OwnProps { - bar: number - } - - interface MapStateProps { - foo: string - } - - class Component extends React.Component { - static defaultProps = { - bar: 0, - } - - static meaningOfLife = 42 - - render() { - return
- } - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - } - } - - Component.meaningOfLife - Component.defaultProps.bar - - const ConnectedComponent = connect(mapStateToProps)(Component) - - // This is a non-React static and should be hoisted as-is. - ConnectedComponent.meaningOfLife - - // This is a React static, so it's not hoisted. - // However, ConnectedComponent is still a ComponentClass, which specifies `defaultProps` - // as an optional static member. We can force an error (and assert that `defaultProps` - // wasn't hoisted) by reaching into the `defaultProps` object without a null check. - // @ts-expect-error - ConnectedComponent.defaultProps.bar -} - -function TestProviderContext() { - const store: Store = createStore((state = {}) => state) - const nullContext = React.createContext(null) - - // To ensure type safety when consuming the context in an app, a null-context does not suffice. - // @ts-expect-error - ; - ; -
- - - // react-redux exports a default context used internally if none is supplied, used as shown below. - class ComponentWithDefaultContext extends React.Component { - static contextType = ReactReduxContext - } - - // eslint-disable-next-line no-extra-semi - ; - - - - // Null is not a valid value for the context. - // @ts-expect-error - ; -} - -function testConnectedProps() { - interface OwnProps { - own: string - } - const Component: React.FC = ({ own, dispatch }) => null - - const connector = connect() - type ReduxProps = ConnectedProps - - const ConnectedComponent = connect(Component) -} - -function testConnectedPropsWithState() { - interface OwnProps { - own: string - } - const Component: React.FC = ({ - own, - injected, - dispatch, - }) => { - injected.slice() - return null - } - - const connector = connect((state: any) => ({ injected: '' })) - type ReduxProps = ConnectedProps - - const ConnectedComponent = connect(Component) -} - -function testConnectedPropsWithStateAndActions() { - interface OwnProps { - own: string - } - const actionCreator = () => ({ type: 'action' }) - - const Component: React.FC = ({ - own, - injected, - actionCreator, - }) => { - actionCreator() - return null - } - - const ComponentWithDispatch: React.FC = ({ - own, - // @ts-expect-error - dispatch, - }) => null - - const connector = connect((state: any) => ({ injected: '' }), { - actionCreator, - }) - type ReduxProps = ConnectedProps - - const ConnectedComponent = connect(Component) -} - -function testConnectReturnType() { - const TestComponent: React.FC = () => null - - const Test = connect()(TestComponent) - - const myHoc1 = (C: React.ComponentClass

): React.ComponentType

=> C - // @ts-expect-error - myHoc1(Test) - - const myHoc2 = (C: React.FC

): React.ComponentType

=> C - // TODO Figure out the error here - // myHoc2(Test) -} - -function testRef() { - const FunctionalComponent: React.FC = () => null - const ForwardedFunctionalComponent = React.forwardRef(() => null) - class ClassComponent extends React.Component {} - - const ConnectedFunctionalComponent = connect()(FunctionalComponent) - const ConnectedForwardedFunctionalComponent = connect()( - ForwardedFunctionalComponent, - ) - const ConnectedClassComponent = connect()(ClassComponent) - - // Should not be able to pass any type of ref to a FunctionalComponent - // ref is not a valid property - ;()} - > - ; {}} - > - - // @ts-expect-error - ; - - // Should be able to pass modern refs to a ForwardRefExoticComponent - const modernRef: React.Ref | undefined = undefined - ; - // Should not be able to use legacy string refs - ; - // ref type should agree with type of the forwarded ref - ;()} - > - ; {}} - > - - // Should be able to use all refs (except legacy string refs, which go away in React 19) - const classLegacyRef: React.Ref | undefined = undefined - ; - ;()} - > - ; {}} - > - // TODO Can make this an expected error if we target React 19 exclusively - // ; - // ref type should be the typeof the wrapped component - ;()} - > - // @ts-expect-error - ; {}}> -} - -function testConnectDefaultState() { - connect((state) => { - const s = state - expectType(s) - return state - }) - - const connectWithDefaultState: Connect<{ value: number }> = connect - connectWithDefaultState((state) => { - const s = state - expectType<{ value: number }>(state) - return state - }) -} - -function testPreserveDiscriminatedUnions() { - type OwnPropsT = { - color: string - } & ( - | { - type: 'plain' - } - | { - type: 'localized' - params: Record | undefined - } - ) - - class MyText extends React.Component {} - - const ConnectedMyText = connect()(MyText) - const someParams = { key: 'value', foo: 'bar' } - - ; - // @ts-expect-error - ; - // @ts-expect-error - ; - ; -} - -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 -} diff --git a/test/typetests/hooks.test-d.tsx b/test/typetests/hooks.test-d.tsx new file mode 100644 index 000000000..b1c7326b9 --- /dev/null +++ b/test/typetests/hooks.test-d.tsx @@ -0,0 +1,277 @@ +import type { AnyAction, Dispatch, Store } from '@reduxjs/toolkit' +import { configureStore } from '@reduxjs/toolkit' +import { createContext } from 'react' +import type { + ReactReduxContextValue, + Selector, + TypedUseSelectorHook, + UseSelector, +} from 'react-redux' +import { + createDispatchHook, + createSelectorHook, + createStoreHook, + shallowEqual, + useDispatch, + useSelector, + useStore, +} from 'react-redux' +import type { AppDispatch, RootState } from './counterApp' +import { incrementAsync } from './counterApp' + +describe('type tests', () => { + test('pre-typed hooks setup', () => { + // Standard hooks setup + const useAppDispatch = () => useDispatch() + + const useAppSelector: TypedUseSelectorHook = useSelector + + function CounterComponent() { + const dispatch = useAppDispatch() + + return ( +