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/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/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"