diff --git a/package.json b/package.json index 821315950e..f433c2183f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "test-coverage": "run-s test-build-coverage test-serve-coverage", "test-alex": "alex ./docs-* *.md", "test-lockfile": "lockfile-lint --path yarn.lock --allowed-hosts yarn --validate-https", - "test-debug": "node --inspect-brk node_modules/.bin/jest --runInBand" + "test-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", + "ts": "tsc" }, "license": "MPL-2.0", "repository": { @@ -44,6 +45,7 @@ "url": "https://github.com/firefox-devtools/profiler" }, "dependencies": { + "@types/react-redux": "^7.1.11", "array-move": "^3.0.1", "array-range": "^1.0.1", "clamp": "^1.0.1", @@ -71,6 +73,7 @@ "redux-logger": "^3.0.6", "redux-thunk": "^2.2.0", "reselect": "^4.0.0", + "typescript": "^4.0.5", "url": "^0.11.0", "weaktuplemap": "^1.0.0" }, diff --git a/src/components/app/DragAndDrop.js b/src/components/app/DragAndDrop.js index 9c6dea2f02..f5ed80a33a 100644 --- a/src/components/app/DragAndDrop.js +++ b/src/components/app/DragAndDrop.js @@ -7,8 +7,7 @@ import * as React from 'react'; import classNames from 'classnames'; import { retrieveProfileFromFile } from 'firefox-profiler/actions/receive-profile'; -import type { ConnectedProps } from 'firefox-profiler/utils/connect'; -import explicitConnect from 'firefox-profiler/utils/connect'; +import { connect } from 'react-redux'; import { startDragging, @@ -21,6 +20,7 @@ import { getIsDragAndDropOverlayRegistered, getIsNewProfileLoadAllowed, } from 'firefox-profiler/selectors/app'; +import type { ConnectedProps, State } from 'firefox-profiler/types'; import './DragAndDrop.css'; @@ -44,6 +44,17 @@ type DispatchProps = {| +stopDragging: typeof stopDragging, |}; +const mapStateToProps = (state: State): StateProps => ({ + isNewProfileLoadAllowed: getIsNewProfileLoadAllowed(state), + useDefaultOverlay: !getIsDragAndDropOverlayRegistered(state), +}); + +const mapDispatchToProps: DispatchProps = { + retrieveProfileFromFile, + startDragging, + stopDragging, +}; + type Props = ConnectedProps; /** @@ -139,20 +150,10 @@ class DragAndDropImpl extends React.PureComponent { } } -export const DragAndDrop = explicitConnect( - { - mapStateToProps: state => ({ - isNewProfileLoadAllowed: getIsNewProfileLoadAllowed(state), - useDefaultOverlay: !getIsDragAndDropOverlayRegistered(state), - }), - mapDispatchToProps: { - retrieveProfileFromFile, - startDragging, - stopDragging, - }, - component: DragAndDropImpl, - } -); +export const DragAndDrop = connect( + mapStateToProps, + mapDispatchToProps +)(DragAndDropImpl); type OverlayOwnProps = {| +isDefault?: boolean, @@ -207,18 +208,21 @@ class DragAndDropOverlayImpl extends React.PureComponent { } } -export const DragAndDropOverlay = explicitConnect< +const overlayMapStateToProps = (state: State): OverlayStateProps => ({ + isDragging: getIsDragAndDropDragging(state), + isNewProfileLoadAllowed: getIsNewProfileLoadAllowed(state), +}); + +const overlayMapDispatchToProps: OverlayDispatchProps = { + registerDragAndDropOverlay, + unregisterDragAndDropOverlay, +}; + +export const DragAndDropOverlay = connect< OverlayOwnProps, OverlayStateProps, OverlayDispatchProps ->({ - mapStateToProps: state => ({ - isDragging: getIsDragAndDropDragging(state), - isNewProfileLoadAllowed: getIsNewProfileLoadAllowed(state), - }), - mapDispatchToProps: { - registerDragAndDropOverlay, - unregisterDragAndDropOverlay, - }, - component: DragAndDropOverlayImpl, -}); +>( + overlayMapStateToProps, + overlayMapDispatchToProps +)(DragAndDropOverlayImpl); diff --git a/src/test/types/react-redux-connect.js b/src/test/types/react-redux-connect.js new file mode 100644 index 0000000000..df527b49f4 --- /dev/null +++ b/src/test/types/react-redux-connect.js @@ -0,0 +1,240 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ +// @flow + +import * as React from 'react'; +import { connect } from 'react-redux'; + +import type { + State, + Action, + ThunkAction, + Dispatch, + GetState, + ConnectedProps, +} from 'firefox-profiler/types'; +/* eslint-disable no-unused-vars */ +import { + coerce, + coerceThunk0, + coerceThunk1, + coerceThunk2, +} from '../../utils/flow'; + +// Use this any value to create fake variables as a type. Consider using +// `declare var myVariables: MyType;` instead. However, it can sometimes be clearer to +// create values inline, or from pre-existing type definitions. In addition, +// `declare var` is not correctly lexically scoped. +const ANY_VALUE = coerce(0); +const numberValue: number = 5; +const stringValue: string = 'string'; + +/** + * These type tests create various values that should all type check correctly to show + * that the react-redux system is working correctly. + */ + +type OwnProps = {| + +ownPropString: string, + +ownPropNumber: number, +|}; + +type StateProps = {| + +statePropString: string, + +statePropNumber: number, +|}; + +type ExampleActionCreator = string => Action; +type ExampleThunkActionCreator = string => ThunkAction; + +type DispatchProps = {| + +dispatchString: ExampleActionCreator, + // TODO - Switch to TypeScript and use a coerceThunk strategy, and infer the + // DispatchProps. + +dispatchThunk: ExampleThunkActionCreator, +|}; + +type Props = ConnectedProps; + +class ExampleComponent extends React.PureComponent { + render() { + // Ensure that the React component has the correct types inside of it. + (this.props.ownPropString: string); + (this.props.ownPropNumber: number); + (this.props.statePropString: string); + (this.props.statePropNumber: number); + + // The action creators are properly wrapped by dispatch. + (this.props.dispatchString: string => Action); + + // DispatchThunk is currently broken, but can be properly fixed by inferring + // the type using a DeThunk strategy if and when we switch to TypeScript. + // + // ```js + // const mapStateToProps = (state: State) => ({ + // foo: getFoo(state), + // bar: getBar(state), + // }); + + // const mapDispatchToProps = { + // // Fakes the transform: + // // () => (D, GS) => Return + // // () => Return + // doThunkAction0: coerceThunk0(doThunkAction0), + // // Fakes the transform: + // // (A) => (D, GS) => Return + // // (A) => Return + // doThunkAction1: coerceThunk1(doThunkAction1), + // // Fakes the transform: + // // (A, B) => (D, GS) => Return + // // (A, B) => Return + // doThunkAction2: coerceThunk2(doThunkAction2), + // }; + + // type StateProps = ReturnType; + // type DispatchProps = typeof mapDispatchToProps; + // type Props = ConnectedProps<{}, StateProps, DispatchProps>; + // ``` + + // $FlowFixMe - See comment above + (this.props.dispatchThunk: string => number); + // $FlowFixMe - See comment above + (this.props.dispatchThunk('foo'): number); + + return null; + } +} + +const validMapStateToProps = (state: State, ownProps: OwnProps): StateProps => { + return { + statePropString: 'string', + statePropNumber: 0, + }; +}; + +type DispatchToProps = {| + +dispatchString: string => Action, + +dispatchThunk: string => ThunkAction, +|}; + +declare var validDispatchToProps: DispatchToProps; + +// This value also serves as a test for the common case of creating a component +// with valid values. +const ConnectedExampleComponent = connect( + validMapStateToProps, + validDispatchToProps +)(ExampleComponent); + +type DeThunk0Expected = () => number; +type DeThunk1Expected = (a: string) => number; +type DeThunk2Expected = (a: string, b: number) => number; +{ + // Test the de-thunk process. + const thunk0 = () => (dispatch: Dispatch, getState: GetState) => numberValue; + const thunk1 = (a: string) => (dispatch: Dispatch, getState: GetState) => + numberValue; + const thunk2 = (a: string, b: number) => ( + dispatch: Dispatch, + getState: GetState + ) => numberValue; + + const deThunk0Actual: DeThunk0Expected = coerceThunk0(thunk0); + const deThunk1Actual: DeThunk1Expected = coerceThunk1(thunk1); + const deThunk2Actual: DeThunk2Expected = coerceThunk2(thunk2); + + // $FlowExpectError + const deThunk0Error: () => void = coerceThunk0(thunk0); + // $FlowExpectError + const deThunk1Error: () => void = coerceThunk1(thunk0); + // $FlowExpectError + const deThunk2Error: () => void = coerceThunk0(thunk0); +} + +{ + // Test that mapStateToProps will error out if provided an extra value. + connect( + // $FlowExpectError + (state: State): StateProps => ({ + statePropString: 'string', + statePropNumber: 0, + extraValue: null, + }), + validDispatchToProps + )(ExampleComponent); +} + +{ + // Test that mapStateToProps will error if provided an extra value. + connect( + (state: State): StateProps => ({ + statePropString: 'string', + // $FlowExpectError + statePropNumber: 'not a number', + }), + validDispatchToProps + )(ExampleComponent); +} + +{ + // Test that mapDispatchToProps will error if a value is omitted. + connect( + validMapStateToProps, + // $FlowExpectError + (ANY_VALUE: {| + +dispatchThunk: string => ThunkAction, + |}) + )(ExampleComponent); +} + +{ + // Test that mapDispatchToProps will error if a variable type definition is wrong. + connect( + validMapStateToProps, + (ANY_VALUE: {| + // $FlowExpectError + +dispatchString: string => string, + +dispatchThunk: string => ThunkAction, + |}) + )(ExampleComponent); +} + +{ + // Test that mapDispatchToProps will error if an extra property is given. + connect( + validMapStateToProps, + // $FlowExpectError + (ANY_VALUE: {| + ...typeof validDispatchToProps, + +extraProperty: string => string, + |}) + )(ExampleComponent); +} + +{ + // The connected component correctly takes OwnProps. + ; +} + +{ + // The connected component must not accept more props. + // $FlowExpectError + ; +} + +{ + // It throws an error when an OwnProps is incorrect. + // $FlowExpectError + ; +} + +{ + // It throws an error if no OwnProps are provided. + // $FlowExpectError + ; +} diff --git a/src/test/types/react-redux.js b/src/test/types/react-redux-explicit-connect.js similarity index 100% rename from src/test/types/react-redux.js rename to src/test/types/react-redux-explicit-connect.js diff --git a/src/test/typescript/profiler-connect.tsx b/src/test/typescript/profiler-connect.tsx new file mode 100644 index 0000000000..5e72f2ef6c --- /dev/null +++ b/src/test/typescript/profiler-connect.tsx @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import * as React from 'react'; + +import { + State, + Action, + ThunkAction, + ConnectedProps, + DeThunkObj, +} from './types'; + +import { assertValueHasType, typeToValue, connect } from './utils'; + +/** + * These type tests create various values that should all type check correctly to show + * that the react-redux system is working correctly. + */ + +type OwnProps = { + ownPropString: string; + ownPropNumber: number; +}; + +type StateProps = { + statePropString: string; + statePropNumber: number; +}; + +type ExampleActionCreator = (a: string) => Action; +type ExampleThunkActionCreator = (a: string) => ThunkAction; + +type DispatchProps = { + dispatchString: ExampleActionCreator; + dispatchThunkNumber: ExampleThunkActionCreator; +}; + +type Props = ConnectedProps; + +class ExampleComponent extends React.PureComponent { + render() { + // Ensure that the React component has the correct types inside of it. + assertValueHasType(this.props.ownPropString); + assertValueHasType(this.props.ownPropNumber); + assertValueHasType(this.props.statePropString); + assertValueHasType(this.props.statePropNumber); + + // The action creators are properly wrapped by dispatch. + assertValueHasType<(a: string) => Action>(this.props.dispatchString); + assertValueHasType<(a: string) => number>(this.props.dispatchThunkNumber); + assertValueHasType(this.props.dispatchThunkNumber('foo')); + + assertValueHasType<(a: string) => number>(this.props.dispatchThunkNumber); + assertValueHasType(this.props.dispatchThunkNumber('foo')); + + return null; + } +} + +const validMapStateToProps = (state: State, ownProps: OwnProps): StateProps => { + return { + statePropString: 'string', + statePropNumber: 0, + }; +}; + +const mapStateToPropsNoOwnProps = (state: State): StateProps => { + return { + statePropString: 'string', + statePropNumber: 0, + }; +}; + +const dispatchString = typeToValue<(a: string) => Action>(); +const dispatchThunkNumber = typeToValue<(a: string) => ThunkAction>(); + +const validDispatchToProps = { + dispatchString, + dispatchThunkNumber: dispatchThunkNumber, +}; + +const ConnectedExampleComponent = connect( + validMapStateToProps, + validDispatchToProps +)(ExampleComponent); + +{ + // Test mapStateToProps with no OwnProps. + connect( + mapStateToPropsNoOwnProps, + validDispatchToProps + )(ExampleComponent); +} + +{ + // Test de-Thunking an object + type DispatchPropsThunked = { + action: (a: string) => Action; + thunk0: () => ThunkAction; + thunk1: (a: string) => ThunkAction; + thunk2: (a: string, b: number) => ThunkAction; + }; + + type ExpectedDispatchPropsDeThunked = { + action: (a: string) => Action; + thunk0: () => number; + thunk1: (a: string) => number; + thunk2: (a: string, b: number) => number; + }; + + type ActualDispatchPropsDeThunked = DeThunkObj; + + assertValueHasType( + typeToValue() + ); +} + +{ + connect( + (state: State): StateProps => ({ + statePropString: 'string', + statePropNumber: 0, + // @ts-expect-error - Extra value in StateProps + extraValue: null, + }), + validDispatchToProps + )(ExampleComponent); +} + +{ + connect( + (state: State): StateProps => ({ + statePropString: 'string', + // @ts-expect-error - Expected a number, not a string. + statePropNumber: 'not a number', + }), + validDispatchToProps + )(ExampleComponent); +} + +{ + const missingValueProps = typeToValue<{ + dispatchThunkNumber: (a: string) => ThunkAction; + }>(); + + connect( + validMapStateToProps, + // @ts-expect-error - mapDispatchToProps will error if a value is omitted. + missingValueProps + )(ExampleComponent); +} + +{ + const wrongProps = typeToValue<{ + dispatchString: (a: string) => string; + dispatchThunkNumber: (a: string) => ThunkAction; + }>(); + + connect( + validMapStateToProps, + // @ts-expect-error - mapDispatchToProps will error if a variable type definition is wrong. + wrongProps + )(ExampleComponent); +} + +// TODO - This assertion is not working. +// +// { +// const extraValueProps = typeToValue< +// DispatchProps & { +// extraProperty: (a: string) => string; +// } +// >(); +// connect( +// validMapStateToProps, +// // @ts-expect-error - mapDispatchToProps will error if an extra property is given. +// extraValueProps +// )(ExampleComponent); +// } + +{ + const deThunkedDispatchProps = typeToValue>(); + connect( + validMapStateToProps, + // @ts-expect-error - De-thunking dispatch props early on is an error. + deThunkedDispatchProps + )(ExampleComponent); +} + +{ + // The connected component correctly takes OwnProps. + ; +} + +{ + ; +} + +{ + // @ts-expect-error - It throws an error when an OwnProps is incorrect. + ; +} + +{ + // @ts-expect-error - It throws an error if no OwnProps are provided. + ; +} diff --git a/src/test/typescript/types.ts b/src/test/typescript/types.ts new file mode 100644 index 0000000000..92a29fc5f4 --- /dev/null +++ b/src/test/typescript/types.ts @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +// Stub out the store types. +export interface State { + fake: 'example'; +} +export type Action = { type: 'FAKE_EXAMPLE1' } | { type: 'FAKE_EXAMPLE2' }; + +export type ConnectedProps< + OwnProps, + StateProps, + DispatchProps extends DispatchPropsBounds +> = Readonly>; + +type ThunkDispatch = (action: ThunkAction) => Returns; +export type PlainDispatch = (action: Action) => Action; +export type GetState = () => State; + +/** + * A thunk action. + */ +export type ThunkAction = ( + dispatch: Dispatch, + getState: GetState +) => Returns; + +/** + * The `dispatch` function can accept either a plain action or a thunk action. + * This is similar to a type `(action: Action | ThunkAction) => any` except this + * allows to type the return value as well. + */ +export type Dispatch = PlainDispatch & ThunkDispatch; + +/** + * Remove the (GetState, Dispatch) from the ThunkAction. + * + * For example: + * From: (...args) => (GetState, Dispatch) => Return + * To: (...args) => Return + * + * Or more simply: + * From: (...args) => ThunkAction + * To: (...args) => Return + */ +export type DeThunk ThunkAction> = ( + ...args: Parameters +) => ReturnType>; + +type ActionCreatorBounds = (...args: any[]) => Action; +type ThunkActionCreatorBounds = (...args: any[]) => ThunkAction; + +/** + * Use this to extend a generic. + * + * For example: + * type MyType + */ +export type DispatchPropsBounds = { + [key: string]: ActionCreatorBounds | ThunkActionCreatorBounds; +}; + +/** + * Apply DeThunk to an object, like DispatchProps. + */ +// prettier-ignore +export type DeThunkObj< + DispatchProps extends DispatchPropsBounds +> = { + [Key in keyof DispatchProps]: + DispatchProps[Key] extends ThunkActionCreatorBounds + ? DeThunk + : DispatchProps[Key]; +}; diff --git a/src/test/typescript/utils.ts b/src/test/typescript/utils.ts new file mode 100644 index 0000000000..1d13980cec --- /dev/null +++ b/src/test/typescript/utils.ts @@ -0,0 +1,67 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at . */ + +import { State, DeThunkObj, DispatchPropsBounds } from './types'; +import { connect as reactReduxConnect } from 'react-redux'; + +/** + * Convert a type into a fake value, which is useful for writing tests. + */ +export function typeToValue(): T { + return null as any; +} + +/** + * Create a type assertion on a value, which is useful to test if a type is working + * as expected. + * + * TODO - It would also be nice to create an AssertEqual type, but I'm not + * sure how to do it at this time. + */ +export function assertValueHasType(_assertion: A): void {} + +/** + * Coerce one type explicitly to another. Just using "as" will attempt to match + * the previous object to the new one. This is a somewhat loose transformation. + * This function allows for an explicit escape hatch where one type is explicitly + * transformed into another. This has a benefit that the coercion is made explicit. + */ +export function coerce(item: A): B { + return item as any; +} + +/** + * Provide a custom connect function that wires into the default react-redux one. + * + * It maintains the legacy ordering from the Flow to TS migration. + * + * Legacy (Flow) ordering: + * + * + * And maps it to the built-in ordering of: + * + * + * In addition, it applies the de-thunking strategy of removing the (GetState, Dispatch) + * from the Thunk action signature. + */ +export function connect< + OwnProps, + StateProps, + ThunkedDispatch extends DispatchPropsBounds +>( + mapStateToProps: + | ((state: State, ownProps: OwnProps) => StateProps) + | ((state: State) => StateProps), + mapDispatchToProps: ThunkedDispatch +) { + return reactReduxConnect< + StateProps, + DeThunkObj, + OwnProps, + State + >( + mapStateToProps, + coerce>(mapDispatchToProps) + ); +} diff --git a/src/types/libdef/npm/react-redux_v7.x.x.js b/src/types/libdef/npm/react-redux_v7.x.x.js index 7b2d52d77e..4ae68e7d86 100644 --- a/src/types/libdef/npm/react-redux_v7.x.x.js +++ b/src/types/libdef/npm/react-redux_v7.x.x.js @@ -32,6 +32,8 @@ Decrypting the abbreviations: EFO = Extra factory options (used only in connectAdvanced) */ +import type { State, PlainDispatch, ConnectedProps } from 'firefox-profiler/types'; + declare module "react-redux" { // ------------------------------------------------------------ // Typings for connect() @@ -87,55 +89,55 @@ declare module "react-redux" { declare type MergeOPDP = {| ...$Exact, ...DP |}; declare type MergeOPSPDP = {| ...$Exact, ...SP, ...DP |}; - declare export function connect<-P, -OP, -SP, -DP, -S, -D>( + declare export function connect<-OP, -SP, -DP>( mapStateToProps?: null | void, mapDispatchToProps?: null | void, mergeProps?: null | void, - options?: ?Options>, - ): Connector>; + options?: ?Options>, + ): Connector, OP, MergeOP>; - declare export function connect<-P, -OP, -SP, -DP, -S, -D>( + declare export function connect<-OP, -SP, -DP>( // If you get error here try adding return type to your mapStateToProps function - mapStateToProps: MapStateToProps, + mapStateToProps: MapStateToProps, mapDispatchToProps?: null | void, mergeProps?: null | void, - options?: ?Options>, - ): Connector>; + options?: ?Options>, + ): Connector, OP, MergeOPSP>; // In this case DP is an object of functions which has been bound to dispatch // by the given mapDispatchToProps function. - declare export function connect<-P, -OP, -SP, -DP, S, D>( + declare export function connect<-OP, -SP, -DP>( mapStateToProps: null | void, - mapDispatchToProps: MapDispatchToPropsFn, + mapDispatchToProps: MapDispatchToPropsFn, mergeProps?: null | void, - options?: ?Options>, - ): Connector>; + options?: ?Options>, + ): Connector, OP, MergeOPDP>; // In this case DP is an object of action creators not yet bound to dispatch, // this difference is not important in the vanila redux, // but in case of usage with redux-thunk, the return type may differ. - declare export function connect<-P, -OP, -SP, -DP, S, D>( + declare export function connect<-OP, -SP, -DP>( mapStateToProps: null | void, mapDispatchToProps: DP, mergeProps?: null | void, - options?: ?Options>, - ): Connector>>>; + options?: ?Options>, + ): Connector, OP, MergeOPDP>>>; - declare export function connect<-P, -OP, -SP, -DP, S, D>( + declare export function connect<-OP, -SP, -DP>( // If you get error here try adding return type to your mapStateToProps function - mapStateToProps: MapStateToProps, - mapDispatchToProps: MapDispatchToPropsFn, + mapStateToProps: MapStateToProps, + mapDispatchToProps: MapDispatchToPropsFn, mergeProps?: null | void, - options?: ?Options, - ): Connector; + options?: ?Options, + ): Connector, OP, {| ...OP, ...SP, ...DP |}>; - declare export function connect<-P, -OP, -SP, -DP, S, D>( + declare export function connect<-OP, -SP, -DP>( // If you get error here try adding return type to your mapStateToProps function - mapStateToProps: MapStateToProps, + mapStateToProps: MapStateToProps, mapDispatchToProps: DP, mergeProps?: null | void, - options?: ?Options>, - ): Connector>>>; + options?: ?Options>, + ): Connector, OP, MergeOPSPDP>>>; // With `mergeProps` argument @@ -145,59 +147,59 @@ declare module "react-redux" { ownProps: OP, ) => P; - declare export function connect<-P, -OP, -SP: {||}, -DP: {||}, S, D>( + declare export function connect<-OP, -SP: {||}, -DP: {||}>( mapStateToProps: null | void, mapDispatchToProps: null | void, // If you get error here try adding return type to you mapStateToProps function - mergeProps: MergeProps, - options?: ?Options, - ): Connector; + mergeProps: MergeProps, OP, {||}, {| dispatch: PlainDispatch |}>, + options?: ?Options>, + ): Connector, OP, ConnectedProps>; - declare export function connect<-P, -OP, -SP, -DP: {||}, S, D>( - mapStateToProps: MapStateToProps, + declare export function connect<-OP, -SP, -DP: {||}>( + mapStateToProps: MapStateToProps, mapDispatchToProps: null | void, // If you get error here try adding return type to you mapStateToProps function - mergeProps: MergeProps, - options?: ?Options, - ): Connector; + mergeProps: MergeProps, OP, SP, {| dispatch: PlainDispatch |}>, + options?: ?Options>, + ): Connector, OP, ConnectedProps>; // In this case DP is an object of functions which has been bound to dispatch // by the given mapDispatchToProps function. - declare export function connect<-P, -OP, -SP: {||}, -DP, S, D>( + declare export function connect<-OP, -SP: {||}, -DP>( mapStateToProps: null | void, - mapDispatchToProps: MapDispatchToPropsFn, - mergeProps: MergeProps, - options?: ?Options, - ): Connector; + mapDispatchToProps: MapDispatchToPropsFn, + mergeProps: MergeProps, OP, {||}, DP>, + options?: ?Options>, + ): Connector, OP, ConnectedProps>; // In this case DP is an object of action creators not yet bound to dispatch, // this difference is not important in the vanila redux, // but in case of usage with redux-thunk, the return type may differ. - declare export function connect<-P, -OP, -SP: {||}, -DP, S, D>( + declare export function connect<-OP, -SP: {||}, -DP>( mapStateToProps: null | void, mapDispatchToProps: DP, - mergeProps: MergeProps>>, - options?: ?Options, - ): Connector; + mergeProps: MergeProps, OP, {||}, $ObjMap>>, + options?: ?Options>, + ): Connector, OP, ConnectedProps>; // In this case DP is an object of functions which has been bound to dispatch // by the given mapDispatchToProps function. - declare export function connect<-P, -OP, -SP, -DP, S, D>( - mapStateToProps: MapStateToProps, - mapDispatchToProps: MapDispatchToPropsFn, - mergeProps: MergeProps, - options?: ?Options, - ): Connector; + declare export function connect<-OP, -SP, -DP>( + mapStateToProps: MapStateToProps, + mapDispatchToProps: MapDispatchToPropsFn, + mergeProps: MergeProps, OP, SP, DP>, + options?: ?Options>, + ): Connector, OP, ConnectedProps>; // In this case DP is an object of action creators not yet bound to dispatch, // this difference is not important in the vanila redux, // but in case of usage with redux-thunk, the return type may differ. - declare export function connect<-P, -OP, -SP, -DP, S, D>( - mapStateToProps: MapStateToProps, + declare export function connect<-OP, -SP, -DP>( + mapStateToProps: MapStateToProps, mapDispatchToProps: DP, - mergeProps: MergeProps>>, - options?: ?Options, - ): Connector; + mergeProps: MergeProps, OP, SP, $ObjMap>>, + options?: ?Options>, + ): Connector, OP, ConnectedProps>; // ------------------------------------------------------------ // Typings for Hooks diff --git a/src/types/store.js b/src/types/store.js index e16c12fb62..b3f2597c4f 100644 --- a/src/types/store.js +++ b/src/types/store.js @@ -61,7 +61,7 @@ export type DangerousSelectorWithArguments = ( ) => T; type ThunkDispatch = (action: ThunkAction) => Returns; -type PlainDispatch = (action: Action) => Action; +export type PlainDispatch = (action: Action) => Action; export type GetState = () => State; /** @@ -81,3 +81,20 @@ export type Dispatch = PlainDispatch & ThunkDispatch; * of all Actions, as well as specific Dispatch behavior. */ export type Store = ReduxStore; + +/** + * Combines the various props for a react-redux connected component. Disable the + * flowtype/no-weak-types rule, as this is a false-positive. + */ +export type ConnectedProps< + // eslint-disable-next-line flowtype/no-weak-types + OwnProps: Object, + // eslint-disable-next-line flowtype/no-weak-types + StateProps: Object, + // eslint-disable-next-line flowtype/no-weak-types + DispatchProps: Object +> = $ReadOnly<{| + ...OwnProps, + ...StateProps, + ...DispatchProps, +|}>; diff --git a/src/utils/flow.js b/src/utils/flow.js index 7dc504abab..8c2af181cf 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -4,7 +4,7 @@ // @flow import type { TabSlug } from '../app-logic/tabs-handling'; -import type { TransformType } from 'firefox-profiler/types'; +import type { TransformType, Dispatch, GetState } from 'firefox-profiler/types'; /** * This file contains utils that help Flow understand things better. Occasionally @@ -109,6 +109,10 @@ export function coerce(item: A): B { return (item: any); } +export function toAny(item: A): any { + return (item: any); +} + /** * It can be helpful to coerce one type that matches the shape of another. */ @@ -182,3 +186,42 @@ export function ensureExists(item: ?T, message: ?string): T { } return item; } + +/** + * Wrap thunk actions with 0 arguments with this functions when passing it to + * mapDispatchToProps. This will make the function typecheck correctly by removing + * the "thunk" part of the action creaor. + * + * TODO - These will be useful when migrating to TypeScript. + */ +export function coerceThunk0( + thunk: () => (dispatch: Dispatch, getState: GetState) => Returns +): () => Returns { + return toAny(thunk); +} + +/** + * Wrap thunk actions with 1 argument with this functions when passing it to + * mapDispatchToProps. This will make the function typecheck correctly by removing + * the "thunk" part of the action creaor. + * + * TODO - These will be useful when migrating to TypeScript. + */ +export function coerceThunk1( + thunk: (a1: A1) => (dispatch: Dispatch, getState: GetState) => Returns +): (a1: A1) => Returns { + return toAny(thunk); +} + +/** + * Wrap thunk actions with 2 arguments with this functions when passing it to + * mapDispatchToProps. This will make the function typecheck correctly by removing + * the "thunk" part of the action creaor. + * + * TODO - These will be useful when migrating to TypeScript. + */ +export function coerceThunk2( + thunk: (a1: A1, a2: A2) => (dispatch: Dispatch, getState: GetState) => Returns +): (a1: A1, a2: A2) => Returns { + return toAny(thunk); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..6068cb2f97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "moduleResolution": "node", + "module": "esnext", + "allowSyntheticDefaultImports": true, + // Make the type checking as strict as possible. + "strict": true, + // Allow esnext syntax. Otherwise the default is ES5 only. + "target": "esnext", + "lib": ["esnext", "dom"], + // Do not modify the output, allow eslint to do so. + "jsx": "preserve", + "noEmit": true + }, + "include": ["./src/**/*.ts"], + "exclude": [ + "node_modules", + "bin" + ] +} diff --git a/yarn.lock b/yarn.lock index b52ecc3b3e..db62f960f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1515,6 +1515,14 @@ dependencies: "@types/node" "*" +"@types/hoist-non-react-statics@^3.3.0": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/html-minifier-terser@^5.0.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz#551a4589b6ee2cc9c1dff08056128aec29b94880" @@ -1604,11 +1612,34 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3" integrity sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA== +"@types/prop-types@*": + version "15.7.3" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" + integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== + "@types/q@^1.5.1": version "1.5.2" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== +"@types/react-redux@^7.1.11": + version "7.1.11" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.11.tgz#a18e8ab3651e8e8cc94798934927937c66021217" + integrity sha512-OjaFlmqy0CRbYKBoaWF84dub3impqnLJUrz4u8PRjDzaa4n1A2cVmjMV81shwXyAD5x767efhA8STFGJz/r1Zg== + dependencies: + "@types/hoist-non-react-statics" "^3.3.0" + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + redux "^4.0.0" + +"@types/react@*": + version "16.9.56" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.56.tgz#ea25847b53c5bec064933095fc366b1462e2adf0" + integrity sha512-gIkl4J44G/qxbuC6r2Xh+D3CGZpJ+NdWTItAPmZbR5mUS+JQ8Zvzpl0ea5qT/ZT3ZNTUcDKUVqV3xBE8wv/DyQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/responselike@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -6178,7 +6209,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -10747,7 +10778,7 @@ redux-thunk@^2.2.0: resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== -redux@^4.0.5: +redux@^4.0.0, redux@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== @@ -12815,6 +12846,11 @@ typescript@^3.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.5.tgz#ae9dddfd1069f1cb5beb3ef3b2170dd7c1332389" + integrity sha512-ywmr/VrTVCmNTJ6iV2LwIrfG1P+lv6luD8sUJs+2eI9NLGigaN+nUQc13iHqisq7bra9lnmUSYqbJvegraBOPQ== + typeson-registry@^1.0.0-alpha.20: version "1.0.0-alpha.35" resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.35.tgz#b86abfe440e6ee69102eebb0e8c5a916dd182ff9"