diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 000000000..2def1ddbe --- /dev/null +++ b/index.d.ts @@ -0,0 +1,295 @@ +import { + Component, + ComponentClass, + ComponentType, + ReactNode, + StatelessComponent, +} from 'react' + +import { + Action, + Store, + Dispatch, + ActionCreator, +} from 'redux' + +// Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766 +type Diff = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]; +type Omit = Pick>; + +export interface DispatchProp { + dispatch?: Dispatch<{}>; +} + +interface AdvancedComponentDecorator { + (component: ComponentType): ComponentClass; +} + +// Injects props and removes them from the prop requirements. +// Will not pass through the injected props if they are passed in during +// render. Also adds new prop requirements from TNeedsProps. +export interface InferableComponentEnhancerWithProps { +

( + component: ComponentType

+ ): ComponentClass & TNeedsProps> & {WrappedComponent: Component

} +} + +// Injects props and removes them from the prop requirements. +// Will not pass through the injected props if they are passed in during +// render. +export type InferableComponentEnhancer = + InferableComponentEnhancerWithProps + +/** + * Connects a React component to a Redux store. + * + * - Without arguments, just wraps the component, without changing the behavior / props + * + * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior + * is to override ownProps (as stated in the docs), so what remains is everything that's + * not a state or dispatch prop + * + * - When 3rd param is passed, we don't know if ownProps propagate and whether they + * should be valid component props, because it depends on mergeProps implementation. + * As such, it is the user's responsibility to extend ownProps interface from state or + * dispatch props or both when applicable + * + * @param mapStateToProps + * @param mapDispatchToProps + * @param mergeProps + * @param options + */ +export interface Connect { + (): InferableComponentEnhancer; + + ( + mapStateToProps: MapStateToPropsParam + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps, + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps, + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: Options + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: Options + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: Options + ): InferableComponentEnhancerWithProps; + + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps, + options: Options + ): InferableComponentEnhancerWithProps; +} + +/** + * The connect function. See {@type Connect} for details. + */ +export const connect: Connect; + +interface MapStateToProps { + (state: any, ownProps: TOwnProps): TStateProps; +} + +interface MapStateToPropsFactory { + (initialState: any, ownProps: TOwnProps): MapStateToProps; +} + +type MapStateToPropsParam = MapStateToPropsFactory | MapStateToProps | null | undefined; + +interface MapDispatchToPropsFunction { + (dispatch: Dispatch, ownProps: TOwnProps): TDispatchProps; +} + +type MapDispatchToProps = + MapDispatchToPropsFunction | TDispatchProps; + +interface MapDispatchToPropsFactory { + (dispatch: Dispatch, ownProps: TOwnProps): MapDispatchToProps; +} + +type MapDispatchToPropsParam = MapDispatchToPropsFactory | MapDispatchToProps; + +interface MergeProps { + (stateProps: TStateProps, dispatchProps: TDispatchProps, ownProps: TOwnProps): TMergedProps; +} + +interface Options extends ConnectOptions { + /** + * If true, implements shouldComponentUpdate and shallowly compares the result of mergeProps, + * preventing unnecessary updates, assuming that the component is a “pure” component + * and does not rely on any input or state other than its props and the selected Redux store’s state. + * Defaults to true. + * @default true + */ + pure?: boolean; + + /** + * When pure, compares incoming store state to its previous value. + * @default strictEqual + */ + areStatesEqual?: (nextState: any, prevState: any) => boolean; + + /** + * When pure, compares incoming props to its previous value. + * @default shallowEqual + */ + areOwnPropsEqual?: (nextOwnProps: TOwnProps, prevOwnProps: TOwnProps) => boolean; + + /** + * When pure, compares the result of mapStateToProps to its previous value. + * @default shallowEqual + */ + areStatePropsEqual?: (nextStateProps: TStateProps, prevStateProps: TStateProps) => boolean; + + /** + * When pure, compares the result of mergeProps to its previous value. + * @default shallowEqual + */ + areMergedPropsEqual?: (nextMergedProps: TMergedProps, prevMergedProps: TMergedProps) => boolean; +} + +/** + * Connects a React component to a Redux store. It is the base for {@link connect} but is less opinionated about + * how to combine state, props, and dispatch into your final props. It makes no + * assumptions about defaults or memoization of results, leaving those responsibilities to the caller.It does not + * modify the component class passed to it; instead, it returns a new, connected component class for you to use. + * + * @param selectorFactory The selector factory. See {@type SelectorFactory} for details. + * @param connectOptions If specified, further customizes the behavior of the connector. Additionally, any extra + * options will be passed through to your selectorFactory in the factoryOptions argument. + */ +export function connectAdvanced( + selectorFactory: SelectorFactory, + connectOptions?: ConnectOptions & TFactoryOptions +): AdvancedComponentDecorator; + +/** + * Initializes a selector function (during each instance's constructor). That selector function is called any time the + * connector component needs to compute new props, as a result of a store state change or receiving new props. The + * result of selector is expected to be a plain object, which is passed as the props to the wrapped + * component. If a consecutive call to selector returns the same object (===) as its previous + * call, the component will not be re-rendered. It's the responsibility of selector to return that + * previous object when appropriate. + */ +export interface SelectorFactory { + (dispatch: Dispatch, factoryOptions: TFactoryOptions): Selector +} + +export interface Selector { + (state: S, ownProps: TOwnProps): TProps +} + +export interface ConnectOptions { + /** + * Computes the connector component's displayName property relative to that of the wrapped component. Usually + * overridden by wrapper functions. + * + * @default name => 'ConnectAdvanced('+name+')' + * @param componentName + */ + getDisplayName?: (componentName: string) => string + /** + * Shown in error messages. Usually overridden by wrapper functions. + * + * @default 'connectAdvanced' + */ + methodName?: string + /** + * If defined, a property named this value will be added to the props passed to the wrapped component. Its value + * will be the number of times the component has been rendered, which can be useful for tracking down unnecessary + * re-renders. + * + * @default undefined + */ + renderCountProp?: string + /** + * Controls whether the connector component subscribes to redux store state changes. If set to false, it will only + * re-render on componentWillReceiveProps. + * + * @default true + */ + shouldHandleStateChanges?: boolean + /** + * The key of props/context to get the store. You probably only need this if you are in the inadvisable position of + * having multiple stores. + * + * @default 'store' + */ + storeKey?: string + /** + * If true, stores a ref to the wrapped component instance and makes it available via getWrappedInstance() method. + * + * @default false + */ + withRef?: boolean +} + +export interface ProviderProps { + /** + * The single Redux store in your application. + */ + store?: Store; + children?: ReactNode; +} + +/** + * Makes the Redux store available to the connect() calls in the component hierarchy below. + */ +export class Provider extends Component { } + +/** + * Creates a new which will set the Redux Store on the passed key of the context. You probably only need this + * if you are in the inadvisable position of having multiple stores. You will also need to pass the same storeKey to the + * options argument of connect. + * + * @param storeKey The key of the context on which to set the store. + */ +export function createProvider(storeKey: string): typeof Provider; \ No newline at end of file diff --git a/package.json b/package.json index d02f57e74..cf61eee8f 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "./lib/index.js", "module": "es/index.js", "jsnext:main": "es/index.js", + "typings": "./index.d.ts", "scripts": { "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", @@ -47,6 +48,7 @@ }, "homepage": "https://github.com/gaearon/react-redux", "devDependencies": { + "@types/react": "^16.0.18", "babel-cli": "^6.3.17", "babel-core": "^6.3.26", "babel-eslint": "^7.1.1", @@ -98,7 +100,9 @@ "rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-node-resolve": "^3.0.0", "rollup-plugin-replace": "^1.1.1", - "rollup-plugin-uglify": "^2.0.1" + "rollup-plugin-uglify": "^2.0.1", + "typescript": "^2.5.3", + "typescript-definition-tester": "^0.0.5" }, "dependencies": { "hoist-non-react-statics": "^2.2.1", diff --git a/test/typescript.spec.js b/test/typescript.spec.js new file mode 100644 index 000000000..a25551fc2 --- /dev/null +++ b/test/typescript.spec.js @@ -0,0 +1,12 @@ +import * as tt from 'typescript-definition-tester' + +describe('TypeScript definitions', () => { + it('should compile against index.d.ts', (done) => { + tt.compileDirectory( + __dirname + '/typescript', + fileName => fileName.match(/\.tsx?$/), + { strict: true, jsx: true }, + () => done() + ) + }).timeout(5000) +}) \ No newline at end of file diff --git a/test/typescript/connect.tsx b/test/typescript/connect.tsx new file mode 100644 index 000000000..54eb2705b --- /dev/null +++ b/test/typescript/connect.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' +import { Dispatch, Reducer, createStore } from 'redux' +import { Provider, connect } from '../..' + +type State = Todo[] + +interface Todo { + id: number + text: string + completed: boolean +} + +type Action = + | { type: 'ADD_TODO'; text: string } + | { type: 'DELETE_TODO'; id: number } + | { type: 'EDIT_TODO'; id: number; text: string } + | { type: 'COMPLETE_TODO'; id: number } + | { type: 'COMPLETE_ALL' } + | { type: 'CLEAR_COMPLETED' } + +const initialState = [ + { + text: 'Use Redux', + completed: false, + id: 0 + } +] + +const reducer = (state: State = initialState, action: Action) => { + switch (action.type) { + case 'ADD_TODO': + return [ + ...state, + { + id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, + completed: false, + text: action.text, + } + ] + + case 'DELETE_TODO': + return state.filter(todo => + todo.id !== action.id + ) + + case 'EDIT_TODO': + return state.map(todo => + todo.id === action.id ? + { ...todo, text: action.text } : + todo + ) + + case 'COMPLETE_TODO': + return state.map(todo => + todo.id === action.id ? + { ...todo, completed: !todo.completed } : + todo + ) + + case 'COMPLETE_ALL': + const areAllMarked = state.every(todo => todo.completed) + return state.map(todo => ({ + ...todo, + completed: !areAllMarked + })) + + case 'CLEAR_COMPLETED': + return state.filter(todo => todo.completed === false) + + default: + return state + } +} + +const store = createStore(reducer) + +interface AppProps extends State { + dispatch: Dispatch +} + +const App = (props: AppProps) =>

+ +const ConnectedApp = connect( + (state: State) => state, + (dispatch: Dispatch) => ({ dispatch }), +)(App) + +const providedApp = ( + + + +)