diff --git a/.babelrc.js b/.babelrc.js index b6aa67e24f..a6942cad53 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -2,6 +2,7 @@ const { NODE_ENV } = process.env module.exports = { presets: [ + '@babel/typescript', [ '@babel/env', { diff --git a/.eslintignore b/.eslintignore index 7fb555f25d..469147f561 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,5 @@ **/server.js **/webpack.config*.js **/flow-typed/** +# TODO: figure out a way to lint this using flow instead of typescript +examples/todos-flow \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index de792fb8a9..4e01bf55a7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,22 +1,34 @@ module.exports = { extends: 'react-app', + parser: '@typescript-eslint/parser', + + plugins: ['@typescript-eslint'], + settings: { react: { version: '16.8' + }, + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'] + }, + 'import/resolver': { + // use /tsconfig.json + typescript: {} } }, rules: { - 'jsx-a11y/href-no-hash': 'off' - }, - - overrides: [ - { - files: 'test/**/*.js', - env: { - jest: true, - }, - }, - ], + 'jsx-a11y/href-no-hash': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: true, + argsIgnorePattern: '^_' // ignore unused variables whose name is '_' + } + ] + } } diff --git a/.gitignore b/.gitignore index cc152e1c4f..246860cfdc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ -.DS_Store -*.log node_modules + +coverage + dist lib es -coverage +types website/translated_docs website/build/ diff --git a/examples/todomvc/src/components/App.spec.js b/examples/todomvc/src/components/App.spec.js index 2b6144e03d..ed51df865d 100644 --- a/examples/todomvc/src/components/App.spec.js +++ b/examples/todomvc/src/components/App.spec.js @@ -4,8 +4,7 @@ import App from './App' import Header from '../containers/Header' import MainSection from '../containers/MainSection' - -const setup = propOverrides => { +const setup = _propOverrides => { const renderer = createRenderer() renderer.render() const output = renderer.getRenderOutput() @@ -16,16 +15,16 @@ describe('components', () => { describe('Header', () => { it('should render', () => { const output = setup() - const [ header ] = output.props.children + const [header] = output.props.children expect(header.type).toBe(Header) }) }) - + describe('Mainsection', () => { it('should render', () => { const output = setup() - const [ , mainSection ] = output.props.children + const [, mainSection] = output.props.children expect(mainSection.type).toBe(MainSection) }) }) -}) \ No newline at end of file +}) diff --git a/index.d.ts b/index.d.ts index 50e8d018fa..aa8ec2ad31 100644 --- a/index.d.ts +++ b/index.d.ts @@ -211,7 +211,7 @@ export function combineReducers>( * dispatched. */ export interface Dispatch { - (action: T): T + (action: T, ...extraArgs: any[]): T } /** @@ -247,6 +247,17 @@ export type Observer = { next?(value: T): void } +/** + * Extend the state + * + * This is used by store enhancers and store creators to extend state. + * If there is no state extension, it just returns the state, as is, otherwise + * it returns the state joined with its extension. + */ +export type ExtendState = [Extension] extends [never] + ? State + : State & Extension + /** * A store is an object that holds the application's state tree. * There should only be a single store in a Redux app, as the composition @@ -254,8 +265,15 @@ export type Observer = { * * @template S The type of state held by this store. * @template A the type of actions which may be dispatched by this store. + * @template StateExt any extension to state from store enhancers + * @template Ext any extensions to the store from store enhancers */ -export interface Store { +export interface Store< + S = any, + A extends Action = AnyAction, + StateExt = never, + Ext = {} +> { /** * Dispatches an action. It is the only way to trigger a state change. * @@ -326,7 +344,9 @@ export interface Store { * * @param nextReducer The reducer for the store to use instead. */ - replaceReducer(nextReducer: Reducer): void + replaceReducer( + nextReducer: Reducer + ): Store, NewActions, StateExt, Ext> & Ext /** * Interoperability point for observable/reactive libraries. @@ -353,15 +373,15 @@ export type DeepPartial = { * @template StateExt State extension that is mixed into the state type. */ export interface StoreCreator { - ( + ( reducer: Reducer, enhancer?: StoreEnhancer - ): Store & Ext - ( + ): Store, A, StateExt, Ext> & Ext + ( reducer: Reducer, preloadedState?: PreloadedState, enhancer?: StoreEnhancer - ): Store & Ext + ): Store, A, StateExt, Ext> & Ext } /** @@ -415,16 +435,17 @@ export const createStore: StoreCreator * @template Ext Store extension that is mixed into the Store type. * @template StateExt State extension that is mixed into the state type. */ -export type StoreEnhancer = ( - next: StoreEnhancerStoreCreator +export type StoreEnhancer = ( + next: StoreEnhancerStoreCreator ) => StoreEnhancerStoreCreator -export type StoreEnhancerStoreCreator = < + +export type StoreEnhancerStoreCreator = < S = any, A extends Action = AnyAction >( reducer: Reducer, preloadedState?: PreloadedState -) => Store & Ext +) => Store, A, StateExt, Ext> & Ext /* middleware */ @@ -667,3 +688,9 @@ export function compose( ): (...args: any[]) => R export function compose(...funcs: Function[]): (...args: any[]) => R + +export const __DO_NOT_USE__ActionTypes: { + INIT: string + REPLACE: string + PROBE_UNKNOWN_ACTION: () => string +} diff --git a/package-lock.json b/package-lock.json index 1a2d4a32c4..6a2db6a392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -113,6 +113,120 @@ "@babel/types": "^7.4.4" } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz", + "integrity": "sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/helper-replace-supers": "^7.5.5", + "@babel/helper-split-export-declaration": "^7.4.4" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", + "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/generator": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", + "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", + "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", + "dev": true, + "requires": { + "@babel/types": "^7.5.5" + } + }, + "@babel/helper-replace-supers": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", + "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.5.5", + "@babel/helper-optimise-call-expression": "^7.0.0", + "@babel/traverse": "^7.5.5", + "@babel/types": "^7.5.5" + } + }, + "@babel/parser": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", + "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==", + "dev": true + }, + "@babel/traverse": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", + "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.5.5", + "@babel/generator": "^7.5.5", + "@babel/helper-function-name": "^7.1.0", + "@babel/helper-split-export-declaration": "^7.4.4", + "@babel/parser": "^7.5.5", + "@babel/types": "^7.5.5", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", + "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@babel/helper-define-map": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz", @@ -450,6 +564,15 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz", + "integrity": "sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", @@ -757,6 +880,17 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-typescript": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.5.5.tgz", + "integrity": "sha512-pehKf4m640myZu5B2ZviLaiBlxMCjSZ1qTEO459AXKX5GnPueyulJeCqZFs1nz/Ya2dDzXQ1NxZ/kKNWyD4h6w==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.5.5", + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-typescript": "^7.2.0" + } + }, "@babel/plugin-transform-unicode-regex": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", @@ -846,6 +980,16 @@ "@babel/plugin-transform-flow-strip-types": "^7.0.0" } }, + "@babel/preset-typescript": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz", + "integrity": "sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-transform-typescript": "^7.3.2" + } + }, "@babel/register": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.4.4.tgz", @@ -1236,6 +1380,33 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "24.0.17", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.17.tgz", + "integrity": "sha512-1cy3xkOAfSYn78dsBWy4M3h/QF/HeWPchNFDjysVtp3GHeTdSmtluNnELfCmfNRRHo0OWEcpf+NsEJQvwQfdqQ==", + "dev": true, + "requires": { + "@types/jest-diff": "*" + } + }, + "@types/jest-diff": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", + "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/node": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.1.tgz", @@ -1264,12 +1435,12 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.11.0.tgz", - "integrity": "sha512-mXv9ccCou89C8/4avKHuPB2WkSZyY/XcTQUXd5LFZAcLw1I3mWYVjUu6eS9Ja0QkP/ClolbcW9tb3Ov/pMdcqw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz", + "integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "1.11.0", + "@typescript-eslint/experimental-utils": "1.13.0", "eslint-utils": "^1.3.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", @@ -1277,12 +1448,13 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.11.0.tgz", - "integrity": "sha512-7LbfaqF6B8oa8cp/315zxKk8FFzosRzzhF8Kn/ZRsRsnpm7Qcu25cR/9RnAQo5utZ2KIWVgaALr+ZmcbG47ruw==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", + "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "1.11.0", + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "1.13.0", "eslint-scope": "^4.0.0" }, "dependencies": { @@ -1299,21 +1471,21 @@ } }, "@typescript-eslint/parser": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.11.0.tgz", - "integrity": "sha512-5xBExyXaxVyczrZvbRKEXvaTUFFq7gIM9BynXukXZE0zF3IQP/FxF4mPmmh3gJ9egafZFqByCpPTFm3dk4SY7Q==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", + "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.11.0", - "@typescript-eslint/typescript-estree": "1.11.0", + "@typescript-eslint/experimental-utils": "1.13.0", + "@typescript-eslint/typescript-estree": "1.13.0", "eslint-visitor-keys": "^1.0.0" } }, "@typescript-eslint/typescript-estree": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.11.0.tgz", - "integrity": "sha512-fquUHF5tAx1sM2OeRCC7wVxFd1iMELWMGCzOSmJ3pLzArj9+kRixdlC4d5MncuzXpjEqc6045p3KwM0o/3FuUA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", + "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", "dev": true, "requires": { "lodash.unescape": "4.0.1", @@ -2227,6 +2399,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "dev": true + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -2524,10 +2702,38 @@ "resolve": "^1.5.0" } }, + "eslint-import-resolver-typescript": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-1.1.1.tgz", + "integrity": "sha512-jqSfumQ+H5y3FUJ6NjRkbOQSUOlbBucGTN3ELymOtcDBbPjVdm/luvJuCfCaIXGh8sEF26ma1qVdtDgl9ndhUg==", + "dev": true, + "requires": { + "debug": "^4.0.1", + "resolve": "^1.4.0", + "tsconfig-paths": "^3.6.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "eslint-module-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.0.tgz", - "integrity": "sha512-14tltLm38Eu3zS+mt0KvILC3q8jyIAH518MlG+HO0p+yK885Lb1UHTY/UgR91eOyGdmxAPb+OLoW4znqIT6Ndw==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", + "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", "dev": true, "requires": { "debug": "^2.6.8", @@ -2598,9 +2804,9 @@ } }, "eslint-plugin-import": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.0.tgz", - "integrity": "sha512-PZpAEC4gj/6DEMMoU2Df01C5c50r7zdGIN52Yfi7CvvWaYssG7Jt5R9nFG5gmqodxNOz9vQS87xk6Izdtpdrig==", + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", + "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", "dev": true, "requires": { "array-includes": "^3.0.3", @@ -2610,8 +2816,8 @@ "eslint-import-resolver-node": "^0.3.2", "eslint-module-utils": "^2.4.0", "has": "^1.0.3", - "lodash": "^4.17.11", "minimatch": "^3.0.4", + "object.values": "^1.1.0", "read-pkg-up": "^2.0.0", "resolve": "^1.11.0" }, @@ -3172,6 +3378,17 @@ "map-cache": "^0.2.2" } }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, "fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -4958,6 +5175,15 @@ } } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -6576,6 +6802,99 @@ "terser": "^4.1.0" } }, + "rollup-plugin-typescript2": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.24.0.tgz", + "integrity": "sha512-yp7z9hZ5kXjOGXKXkfTRJuIPyLvKtUWfAGmreilnYn+qIVhE5CgE7SFvzLKHggsAXPaCAflHRiEj0l71WXWJkA==", + "dev": true, + "requires": { + "find-cache-dir": "^3.0.0", + "fs-extra": "8.1.0", + "resolve": "1.12.0", + "rollup-pluginutils": "2.8.1", + "tslib": "1.10.0" + }, + "dependencies": { + "find-cache-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.0.0.tgz", + "integrity": "sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.0", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "make-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "resolve": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "rollup-pluginutils": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz", @@ -7265,6 +7584,36 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "tsconfig-paths": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.8.0.tgz", + "integrity": "sha512-zZEYFo4sjORK8W58ENkRn9s+HmQFkkwydDG7My5s/fnfr2YYCaiyXe/HBUcIgU8epEKOXwiahOO+KZYjiXlWyQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "deepmerge": "^2.0.1", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -7272,9 +7621,9 @@ "dev": true }, "tsutils": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz", - "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==", + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -7417,6 +7766,12 @@ } } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/package.json b/package.json index 6dc69e8d77..772f99d6b5 100644 --- a/package.json +++ b/package.json @@ -26,26 +26,27 @@ "main": "lib/redux.js", "unpkg": "dist/redux.js", "module": "es/redux.js", - "typings": "./index.d.ts", + "types": "types/index.d.ts", "files": [ "dist", "lib", "es", "src", - "index.d.ts" + "types" ], "scripts": { - "clean": "rimraf lib dist es coverage", + "clean": "rimraf lib dist es coverage types", "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"", "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"", - "lint": "eslint src test", + "lint": "eslint --ext js,ts src test", + "check-types": "tsc --noEmit", "pretest": "npm run build", "test": "jest", "test:watch": "npm test -- --watch", "test:cov": "npm test -- --coverage", "build": "rollup -c", - "prepare": "npm run clean && npm run format:check && npm run lint && npm test", - "examples:lint": "eslint examples", + "prepare": "npm run clean && npm run check-types && npm run format:check && npm run lint && npm test", + "examples:lint": "eslint --ext js,ts examples", "examples:test": "cross-env CI=true babel-node examples/testAll.js" }, "dependencies": { @@ -60,15 +61,18 @@ "@babel/plugin-proposal-object-rest-spread": "^7.5.4", "@babel/preset-env": "^7.5.4", "@babel/preset-flow": "^7.0.0", + "@babel/preset-typescript": "^7.3.3", "@babel/register": "^7.4.4", - "@typescript-eslint/eslint-plugin": "^1.11.0", - "@typescript-eslint/parser": "^1.11.0", + "@types/jest": "^24.0.17", + "@typescript-eslint/eslint-plugin": "^1.13.0", + "@typescript-eslint/parser": "^1.13.0", "babel-core": "^7.0.0-bridge.0", "babel-eslint": "^10.0.2", "babel-jest": "^24.8.0", "cross-env": "^5.2.0", "eslint": "^5.16.0", "eslint-config-react-app": "^4.0.1", + "eslint-import-resolver-typescript": "^1.1.1", "eslint-plugin-flowtype": "^2.50.3", "eslint-plugin-import": "^2.18.0", "eslint-plugin-jsx-a11y": "^6.2.3", @@ -83,6 +87,7 @@ "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-replace": "^2.2.0", "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-typescript2": "^0.24.0", "rxjs": "^6.5.2", "typescript": "^3.5.3", "typings-tester": "^0.3.2" @@ -102,7 +107,7 @@ ] }, "jest": { - "testRegex": "(/test/.*\\.spec\\.js)$" + "testRegex": "(/test/.*\\.spec\\.[tj]s)$" }, "sideEffects": false } diff --git a/rollup.config.js b/rollup.config.js index 07297f2bc2..27ce0b7bc9 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,42 +1,63 @@ import nodeResolve from 'rollup-plugin-node-resolve' import babel from 'rollup-plugin-babel' import replace from 'rollup-plugin-replace' +import typescript from 'rollup-plugin-typescript2' import { terser } from 'rollup-plugin-terser' import pkg from './package.json' +const noDeclarationFiles = { compilerOptions: { declaration: false } }; + export default [ // CommonJS { - input: 'src/index.js', + input: 'src/index.ts', output: { file: 'lib/redux.js', format: 'cjs', indent: false }, external: [ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {}) ], - plugins: [babel()] + plugins: [ + nodeResolve({ + extensions: ['.ts'] + }), + typescript({ useTsconfigDeclarationDir: true }), + babel() + ] }, // ES { - input: 'src/index.js', + input: 'src/index.ts', output: { file: 'es/redux.js', format: 'es', indent: false }, external: [ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {}) ], - plugins: [babel()] + plugins: [ + nodeResolve({ + extensions: ['.ts'] + }), + typescript({ tsconfigOverride: noDeclarationFiles }), + babel() + ] }, // ES for Browsers { - input: 'src/index.js', + input: 'src/index.ts', output: { file: 'es/redux.mjs', format: 'es', indent: false }, plugins: [ - nodeResolve(), + nodeResolve({ + extensions: ['.ts'] + }), replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), + typescript({ tsconfigOverride: noDeclarationFiles }), + babel({ + exclude: 'node_modules/**' + }), terser({ compress: { pure_getters: true, @@ -50,7 +71,7 @@ export default [ // UMD Development { - input: 'src/index.js', + input: 'src/index.ts', output: { file: 'dist/redux.js', format: 'umd', @@ -58,7 +79,10 @@ export default [ indent: false }, plugins: [ - nodeResolve(), + nodeResolve({ + extensions: ['.ts'] + }), + typescript({ tsconfigOverride: noDeclarationFiles }), babel({ exclude: 'node_modules/**' }), @@ -70,7 +94,7 @@ export default [ // UMD Production { - input: 'src/index.js', + input: 'src/index.ts', output: { file: 'dist/redux.min.js', format: 'umd', @@ -78,7 +102,10 @@ export default [ indent: false }, plugins: [ - nodeResolve(), + nodeResolve({ + extensions: ['.ts'] + }), + typescript({ tsconfigOverride: noDeclarationFiles }), babel({ exclude: 'node_modules/**' }), diff --git a/src/applyMiddleware.js b/src/applyMiddleware.js deleted file mode 100644 index 3bfc647b45..0000000000 --- a/src/applyMiddleware.js +++ /dev/null @@ -1,41 +0,0 @@ -import compose from './compose' - -/** - * Creates a store enhancer that applies middleware to the dispatch method - * of the Redux store. This is handy for a variety of tasks, such as expressing - * asynchronous actions in a concise manner, or logging every action payload. - * - * See `redux-thunk` package as an example of the Redux middleware. - * - * Because middleware is potentially asynchronous, this should be the first - * store enhancer in the composition chain. - * - * Note that each middleware will be given the `dispatch` and `getState` functions - * as named arguments. - * - * @param {...Function} middlewares The middleware chain to be applied. - * @returns {Function} A store enhancer applying the middleware. - */ -export default function applyMiddleware(...middlewares) { - return createStore => (...args) => { - const store = createStore(...args) - let dispatch = () => { - throw new Error( - 'Dispatching while constructing your middleware is not allowed. ' + - 'Other middleware would not be applied to this dispatch.' - ) - } - - const middlewareAPI = { - getState: store.getState, - dispatch: (...args) => dispatch(...args) - } - const chain = middlewares.map(middleware => middleware(middlewareAPI)) - dispatch = compose(...chain)(store.dispatch) - - return { - ...store, - dispatch - } - } -} diff --git a/src/applyMiddleware.ts b/src/applyMiddleware.ts new file mode 100644 index 0000000000..f735843252 --- /dev/null +++ b/src/applyMiddleware.ts @@ -0,0 +1,82 @@ +import compose from './compose' +import { Middleware, MiddlewareAPI } from './types/middleware' +import { AnyAction } from './types/actions' +import { StoreEnhancer, StoreCreator, Dispatch } from './types/store' +import { Reducer } from './types/reducers' + +/** + * Creates a store enhancer that applies middleware to the dispatch method + * of the Redux store. This is handy for a variety of tasks, such as expressing + * asynchronous actions in a concise manner, or logging every action payload. + * + * See `redux-thunk` package as an example of the Redux middleware. + * + * Because middleware is potentially asynchronous, this should be the first + * store enhancer in the composition chain. + * + * Note that each middleware will be given the `dispatch` and `getState` functions + * as named arguments. + * + * @param middlewares The middleware chain to be applied. + * @returns A store enhancer applying the middleware. + * + * @template Ext Dispatch signature added by a middleware. + * @template S The type of the state supported by a middleware. + */ +export default function applyMiddleware(): StoreEnhancer +export default function applyMiddleware( + middleware1: Middleware +): StoreEnhancer<{ dispatch: Ext1 }> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 }> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware, + middleware3: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 }> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware, + middleware3: Middleware, + middleware4: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 }> +export default function applyMiddleware( + middleware1: Middleware, + middleware2: Middleware, + middleware3: Middleware, + middleware4: Middleware, + middleware5: Middleware +): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 & Ext5 }> +export default function applyMiddleware( + ...middlewares: Middleware[] +): StoreEnhancer<{ dispatch: Ext }> +export default function applyMiddleware( + ...middlewares: Middleware[] +): StoreEnhancer { + return (createStore: StoreCreator) => ( + reducer: Reducer, + ...args: any[] + ) => { + const store = createStore(reducer, ...args) + let dispatch: Dispatch = () => { + throw new Error( + 'Dispatching while constructing your middleware is not allowed. ' + + 'Other middleware would not be applied to this dispatch.' + ) + } + + const middlewareAPI: MiddlewareAPI = { + getState: store.getState, + dispatch: (action, ...args) => dispatch(action, ...args) + } + const chain = middlewares.map(middleware => middleware(middlewareAPI)) + dispatch = compose(...chain)(store.dispatch) + + return { + ...store, + dispatch + } + } +} diff --git a/src/bindActionCreators.js b/src/bindActionCreators.ts similarity index 52% rename from src/bindActionCreators.js rename to src/bindActionCreators.ts index 0e47477def..073a3b58df 100644 --- a/src/bindActionCreators.js +++ b/src/bindActionCreators.ts @@ -1,6 +1,16 @@ -function bindActionCreator(actionCreator, dispatch) { - return function() { - return dispatch(actionCreator.apply(this, arguments)) +import { Dispatch } from './types/store' +import { + AnyAction, + ActionCreator, + ActionCreatorsMapObject +} from './types/actions' + +function bindActionCreator( + actionCreator: ActionCreator, + dispatch: Dispatch +) { + return function(this: any, ...args: any[]) { + return dispatch(actionCreator.apply(this, args)) } } @@ -13,19 +23,41 @@ function bindActionCreator(actionCreator, dispatch) { * For convenience, you can also pass an action creator as the first argument, * and get a dispatch wrapped function in return. * - * @param {Function|Object} actionCreators An object whose values are action + * @param actionCreators An object whose values are action * creator functions. One handy way to obtain it is to use ES6 `import * as` * syntax. You may also pass a single function. * - * @param {Function} dispatch The `dispatch` function available on your Redux + * @param dispatch The `dispatch` function available on your Redux * store. * - * @returns {Function|Object} The object mimicking the original object, but with + * @returns The object mimicking the original object, but with * every action creator wrapped into the `dispatch` call. If you passed a * function as `actionCreators`, the return value will also be a single * function. */ -export default function bindActionCreators(actionCreators, dispatch) { +export default function bindActionCreators>( + actionCreator: C, + dispatch: Dispatch +): C + +export default function bindActionCreators< + A extends ActionCreator, + B extends ActionCreator +>(actionCreator: A, dispatch: Dispatch): B + +export default function bindActionCreators< + A, + M extends ActionCreatorsMapObject +>(actionCreators: M, dispatch: Dispatch): M +export default function bindActionCreators< + M extends ActionCreatorsMapObject, + N extends ActionCreatorsMapObject +>(actionCreators: M, dispatch: Dispatch): N + +export default function bindActionCreators( + actionCreators: ActionCreator | ActionCreatorsMapObject, + dispatch: Dispatch +) { if (typeof actionCreators === 'function') { return bindActionCreator(actionCreators, dispatch) } @@ -39,7 +71,7 @@ export default function bindActionCreators(actionCreators, dispatch) { ) } - const boundActionCreators = {} + const boundActionCreators: ActionCreatorsMapObject = {} for (const key in actionCreators) { const actionCreator = actionCreators[key] if (typeof actionCreator === 'function') { diff --git a/src/combineReducers.js b/src/combineReducers.ts similarity index 71% rename from src/combineReducers.js rename to src/combineReducers.ts index bdbcfb2291..07f9f7bd22 100644 --- a/src/combineReducers.js +++ b/src/combineReducers.ts @@ -1,8 +1,16 @@ +import { Reducer } from './types/reducers' +import { AnyAction, Action } from './types/actions' import ActionTypes from './utils/actionTypes' import warning from './utils/warning' import isPlainObject from './utils/isPlainObject' - -function getUndefinedStateErrorMessage(key, action) { +import { + ReducersMapObject, + StateFromReducersMapObject, + ActionFromReducersMapObject +} from './types/reducers' +import { CombinedState } from './types/store' + +function getUndefinedStateErrorMessage(key: string, action: Action) { const actionType = action && action.type const actionDescription = (actionType && `action "${String(actionType)}"`) || 'an action' @@ -15,10 +23,10 @@ function getUndefinedStateErrorMessage(key, action) { } function getUnexpectedStateShapeWarningMessage( - inputState, - reducers, - action, - unexpectedKeyCache + inputState: object, + reducers: ReducersMapObject, + action: Action, + unexpectedKeyCache: { [key: string]: true } ) { const reducerKeys = Object.keys(reducers) const argumentName = @@ -34,9 +42,13 @@ function getUnexpectedStateShapeWarningMessage( } if (!isPlainObject(inputState)) { + const match = Object.prototype.toString + .call(inputState) + .match(/\s([a-z|A-Z]+)/) + const matchType = match ? match[1] : '' return ( `The ${argumentName} has unexpected type of "` + - {}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] + + matchType + `". Expected argument to be an object with the following ` + `keys: "${reducerKeys.join('", "')}"` ) @@ -62,7 +74,7 @@ function getUnexpectedStateShapeWarningMessage( } } -function assertReducerShape(reducers) { +function assertReducerShape(reducers: ReducersMapObject) { Object.keys(reducers).forEach(key => { const reducer = reducers[key] const initialState = reducer(undefined, { type: ActionTypes.INIT }) @@ -100,19 +112,33 @@ function assertReducerShape(reducers) { * into a single state object, whose keys correspond to the keys of the passed * reducer functions. * - * @param {Object} reducers An object whose values correspond to different - * reducer functions that need to be combined into one. One handy way to obtain - * it is to use ES6 `import * as reducers` syntax. The reducers may never return - * undefined for any action. Instead, they should return their initial state - * if the state passed to them was undefined, and the current state for any - * unrecognized action. + * @template S Combined state object type. + * + * @param reducers An object whose values correspond to different reducer + * functions that need to be combined into one. One handy way to obtain it + * is to use ES6 `import * as reducers` syntax. The reducers may never + * return undefined for any action. Instead, they should return their + * initial state if the state passed to them was undefined, and the current + * state for any unrecognized action. * - * @returns {Function} A reducer function that invokes every reducer inside the - * passed object, and builds a state object with the same shape. + * @returns A reducer function that invokes every reducer inside the passed + * object, and builds a state object with the same shape. */ -export default function combineReducers(reducers) { +export default function combineReducers( + reducers: ReducersMapObject +): Reducer> +export default function combineReducers( + reducers: ReducersMapObject +): Reducer, A> +export default function combineReducers>( + reducers: M +): Reducer< + CombinedState>, + ActionFromReducersMapObject +> +export default function combineReducers(reducers: ReducersMapObject) { const reducerKeys = Object.keys(reducers) - const finalReducers = {} + const finalReducers: ReducersMapObject = {} for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] @@ -130,19 +156,22 @@ export default function combineReducers(reducers) { // This is used to make sure we don't warn about the same // keys multiple times. - let unexpectedKeyCache + let unexpectedKeyCache: { [key: string]: true } if (process.env.NODE_ENV !== 'production') { unexpectedKeyCache = {} } - let shapeAssertionError + let shapeAssertionError: Error try { assertReducerShape(finalReducers) } catch (e) { shapeAssertionError = e } - return function combination(state = {}, action) { + return function combination( + state: StateFromReducersMapObject = {}, + action: AnyAction + ) { if (shapeAssertionError) { throw shapeAssertionError } @@ -160,7 +189,7 @@ export default function combineReducers(reducers) { } let hasChanged = false - const nextState = {} + const nextState: StateFromReducersMapObject = {} for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] const reducer = finalReducers[key] diff --git a/src/compose.js b/src/compose.js deleted file mode 100644 index be6cc34ef1..0000000000 --- a/src/compose.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Composes single-argument functions from right to left. The rightmost - * function can take multiple arguments as it provides the signature for - * the resulting composite function. - * - * @param {...Function} funcs The functions to compose. - * @returns {Function} A function obtained by composing the argument functions - * from right to left. For example, compose(f, g, h) is identical to doing - * (...args) => f(g(h(...args))). - */ - -export default function compose(...funcs) { - if (funcs.length === 0) { - return arg => arg - } - - if (funcs.length === 1) { - return funcs[0] - } - - return funcs.reduce((a, b) => (...args) => a(b(...args))) -} diff --git a/src/compose.ts b/src/compose.ts new file mode 100644 index 0000000000..fcbfd70d7b --- /dev/null +++ b/src/compose.ts @@ -0,0 +1,102 @@ +type Func0 = () => R +type Func1 = (a1: T1) => R +type Func2 = (a1: T1, a2: T2) => R +type Func3 = (a1: T1, a2: T2, a3: T3, ...args: any[]) => R + +/** + * Composes single-argument functions from right to left. The rightmost + * function can take multiple arguments as it provides the signature for the + * resulting composite function. + * + * @param funcs The functions to compose. + * @returns A function obtained by composing the argument functions from right + * to left. For example, `compose(f, g, h)` is identical to doing + * `(...args) => f(g(h(...args)))`. + */ +export default function compose(): (a: R) => R + +export default function compose(f: F): F + +/* two functions */ +export default function compose(f1: (b: A) => R, f2: Func0): Func0 +export default function compose( + f1: (b: A) => R, + f2: Func1 +): Func1 +export default function compose( + f1: (b: A) => R, + f2: Func2 +): Func2 +export default function compose( + f1: (b: A) => R, + f2: Func3 +): Func3 + +/* three functions */ +export default function compose( + f1: (b: B) => R, + f2: (a: A) => B, + f3: Func0 +): Func0 +export default function compose( + f1: (b: B) => R, + f2: (a: A) => B, + f3: Func1 +): Func1 +export default function compose( + f1: (b: B) => R, + f2: (a: A) => B, + f3: Func2 +): Func2 +export default function compose( + f1: (b: B) => R, + f2: (a: A) => B, + f3: Func3 +): Func3 + +/* four functions */ +export default function compose( + f1: (b: C) => R, + f2: (a: B) => C, + f3: (a: A) => B, + f4: Func0 +): Func0 +export default function compose( + f1: (b: C) => R, + f2: (a: B) => C, + f3: (a: A) => B, + f4: Func1 +): Func1 +export default function compose( + f1: (b: C) => R, + f2: (a: B) => C, + f3: (a: A) => B, + f4: Func2 +): Func2 +export default function compose( + f1: (b: C) => R, + f2: (a: B) => C, + f3: (a: A) => B, + f4: Func3 +): Func3 + +/* rest */ +export default function compose( + f1: (b: any) => R, + ...funcs: Function[] +): (...args: any[]) => R + +export default function compose(...funcs: Function[]): (...args: any[]) => R + +export default function compose(...funcs: Function[]) { + if (funcs.length === 0) { + // infer the argument type so it is usable in inference down the line + return (arg: T) => arg + } + + if (funcs.length === 1) { + return funcs[0] + } + + return funcs.reduce((a, b) => (...args: any) => a(b(...args))) +} diff --git a/src/createStore.js b/src/createStore.ts similarity index 72% rename from src/createStore.js rename to src/createStore.ts index cef9a2ecae..a75fc2a304 100644 --- a/src/createStore.js +++ b/src/createStore.ts @@ -1,5 +1,15 @@ import $$observable from 'symbol-observable' +import { + Store, + PreloadedState, + StoreEnhancer, + Dispatch, + Observer, + ExtendState +} from './types/store' +import { Action } from './types/actions' +import { Reducer } from './types/reducers' import ActionTypes from './utils/actionTypes' import isPlainObject from './utils/isPlainObject' @@ -11,24 +21,52 @@ import isPlainObject from './utils/isPlainObject' * parts of the state tree respond to actions, you may combine several reducers * into a single reducer function by using `combineReducers`. * - * @param {Function} reducer A function that returns the next state tree, given + * @param reducer A function that returns the next state tree, given * the current state tree and the action to handle. * - * @param {any} [preloadedState] The initial state. You may optionally specify it + * @param preloadedState The initial state. You may optionally specify it * to hydrate the state from the server in universal apps, or to restore a * previously serialized user session. * If you use `combineReducers` to produce the root reducer function, this must be * an object with the same shape as `combineReducers` keys. * - * @param {Function} [enhancer] The store enhancer. You may optionally specify it + * @param enhancer The store enhancer. You may optionally specify it * to enhance the store with third-party capabilities such as middleware, * time travel, persistence, etc. The only store enhancer that ships with Redux * is `applyMiddleware()`. * - * @returns {Store} A Redux store that lets you read the state, dispatch actions + * @returns A Redux store that lets you read the state, dispatch actions * and subscribe to changes. */ -export default function createStore(reducer, preloadedState, enhancer) { +export default function createStore< + S, + A extends Action, + Ext = {}, + StateExt = never +>( + reducer: Reducer, + enhancer?: StoreEnhancer +): Store, A, StateExt, Ext> & Ext +export default function createStore< + S, + A extends Action, + Ext = {}, + StateExt = never +>( + reducer: Reducer, + preloadedState?: PreloadedState, + enhancer?: StoreEnhancer +): Store, A, StateExt, Ext> & Ext +export default function createStore< + S, + A extends Action, + Ext = {}, + StateExt = never +>( + reducer: Reducer, + preloadedState?: PreloadedState | StoreEnhancer, + enhancer?: StoreEnhancer +): Store, A, StateExt, Ext> & Ext { if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') @@ -41,7 +79,7 @@ export default function createStore(reducer, preloadedState, enhancer) { } if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { - enhancer = preloadedState + enhancer = preloadedState as StoreEnhancer preloadedState = undefined } @@ -50,7 +88,9 @@ export default function createStore(reducer, preloadedState, enhancer) { throw new Error('Expected the enhancer to be a function.') } - return enhancer(createStore)(reducer, preloadedState) + return enhancer(createStore)(reducer, preloadedState as PreloadedState< + S + >) as Store, A, StateExt, Ext> & Ext } if (typeof reducer !== 'function') { @@ -58,8 +98,8 @@ export default function createStore(reducer, preloadedState, enhancer) { } let currentReducer = reducer - let currentState = preloadedState - let currentListeners = [] + let currentState = preloadedState as S + let currentListeners: (() => void)[] | null = [] let nextListeners = currentListeners let isDispatching = false @@ -79,9 +119,9 @@ export default function createStore(reducer, preloadedState, enhancer) { /** * Reads the state tree managed by the store. * - * @returns {any} The current state tree of your application. + * @returns The current state tree of your application. */ - function getState() { + function getState(): S { if (isDispatching) { throw new Error( 'You may not call store.getState() while the reducer is executing. ' + @@ -90,7 +130,7 @@ export default function createStore(reducer, preloadedState, enhancer) { ) } - return currentState + return currentState as S } /** @@ -113,10 +153,10 @@ export default function createStore(reducer, preloadedState, enhancer) { * registered before the `dispatch()` started will be called with the latest * state by the time it exits. * - * @param {Function} listener A callback to be invoked on every dispatch. - * @returns {Function} A function to remove this change listener. + * @param listener A callback to be invoked on every dispatch. + * @returns A function to remove this change listener. */ - function subscribe(listener) { + function subscribe(listener: () => void) { if (typeof listener !== 'function') { throw new Error('Expected the listener to be a function.') } @@ -170,18 +210,18 @@ export default function createStore(reducer, preloadedState, enhancer) { * example, see the documentation for the `redux-thunk` package. Even the * middleware will eventually dispatch plain object actions using this method. * - * @param {Object} action A plain object representing “what changed”. It is + * @param action A plain object representing “what changed”. It is * a good idea to keep actions serializable so you can record and replay user * sessions, or use the time travelling `redux-devtools`. An action must have * a `type` property which may not be `undefined`. It is a good idea to use * string constants for action types. * - * @returns {Object} For convenience, the same action object you dispatched. + * @returns For convenience, the same action object you dispatched. * * Note that, if you use a custom middleware, it may wrap `dispatch()` to * return something else (for example, a Promise you can await). */ - function dispatch(action) { + function dispatch(action: A) { if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + @@ -223,26 +263,40 @@ export default function createStore(reducer, preloadedState, enhancer) { * load some of the reducers dynamically. You might also need this if you * implement a hot reloading mechanism for Redux. * - * @param {Function} nextReducer The reducer for the store to use instead. - * @returns {void} + * @param nextReducer The reducer for the store to use instead. + * @returns The same store instance with a new reducer in place. */ - function replaceReducer(nextReducer) { + function replaceReducer( + nextReducer: Reducer + ): Store, NewActions, StateExt, Ext> & Ext { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.') } - currentReducer = nextReducer + // TODO: do this more elegantly + ;((currentReducer as unknown) as Reducer< + NewState, + NewActions + >) = nextReducer // This action has a similiar effect to ActionTypes.INIT. // Any reducers that existed in both the new and old rootReducer // will receive the previous state. This effectively populates // the new state tree with any relevant data from the old one. - dispatch({ type: ActionTypes.REPLACE }) + dispatch({ type: ActionTypes.REPLACE } as A) + // change the type of the store by casting it to the new store + return (store as unknown) as Store< + ExtendState, + NewActions, + StateExt, + Ext + > & + Ext } /** * Interoperability point for observable/reactive libraries. - * @returns {observable} A minimal observable of state changes. + * @returns A minimal observable of state changes. * For more information, see the observable proposal: * https://github.com/tc39/proposal-observable */ @@ -251,20 +305,21 @@ export default function createStore(reducer, preloadedState, enhancer) { return { /** * The minimal observable subscription method. - * @param {Object} observer Any object that can be used as an observer. + * @param observer Any object that can be used as an observer. * The observer object should have a `next` method. - * @returns {subscription} An object with an `unsubscribe` method that can + * @returns An object with an `unsubscribe` method that can * be used to unsubscribe the observable from the store, and prevent further * emission of values from the observable. */ - subscribe(observer) { + subscribe(observer: unknown) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } function observeState() { - if (observer.next) { - observer.next(getState()) + const observerAsObserver = observer as Observer + if (observerAsObserver.next) { + observerAsObserver.next(getState()) } } @@ -282,13 +337,14 @@ export default function createStore(reducer, preloadedState, enhancer) { // When a store is created, an "INIT" action is dispatched so that every // reducer returns their initial state. This effectively populates // the initial state tree. - dispatch({ type: ActionTypes.INIT }) + dispatch({ type: ActionTypes.INIT } as A) - return { - dispatch, + const store = ({ + dispatch: dispatch as Dispatch, subscribe, getState, replaceReducer, [$$observable]: observable - } + } as unknown) as Store, A, StateExt, Ext> & Ext + return store } diff --git a/src/index.js b/src/index.ts similarity index 64% rename from src/index.js rename to src/index.ts index efedbc4fed..40df5f346d 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,3 +1,4 @@ +// functions import createStore from './createStore' import combineReducers from './combineReducers' import bindActionCreators from './bindActionCreators' @@ -6,6 +7,37 @@ import compose from './compose' import warning from './utils/warning' import __DO_NOT_USE__ActionTypes from './utils/actionTypes' +// types +// store +export { + CombinedState, + PreloadedState, + Dispatch, + Unsubscribe, + Observable, + Observer, + Store, + StoreCreator, + StoreEnhancer, + StoreEnhancerStoreCreator, + ExtendState +} from './types/store' +// reducers +export { + Reducer, + ReducerFromReducersMapObject, + ReducersMapObject, + StateFromReducersMapObject, + ActionFromReducer, + ActionFromReducersMapObject +} from './types/reducers' +// action creators +export { ActionCreator, ActionCreatorsMapObject } from './types/actions' +// middleware +export { MiddlewareAPI, Middleware } from './types/middleware' +// actions +export { Action, AnyAction } from './types/actions' + /* * This is a dummy function to check if the function name has been altered by minification. * If the function has been minified and NODE_ENV !== 'production', warn the user. diff --git a/src/types/actions.ts b/src/types/actions.ts new file mode 100644 index 0000000000..75215ed7af --- /dev/null +++ b/src/types/actions.ts @@ -0,0 +1,61 @@ +/** + * An *action* is a plain object that represents an intention to change the + * state. Actions are the only way to get data into the store. Any data, + * whether from UI events, network callbacks, or other sources such as + * WebSockets needs to eventually be dispatched as actions. + * + * Actions must have a `type` field that indicates the type of action being + * performed. Types can be defined as constants and imported from another + * module. It's better to use strings for `type` than Symbols because strings + * are serializable. + * + * Other than `type`, the structure of an action object is really up to you. + * If you're interested, check out Flux Standard Action for recommendations on + * how actions should be constructed. + * + * @template T the type of the action's `type` tag. + */ +export interface Action { + type: T +} + +/** + * An Action type which accepts any other properties. + * This is mainly for the use of the `Reducer` type. + * This is not part of `Action` itself to prevent types that extend `Action` from + * having an index signature. + */ +export interface AnyAction extends Action { + // Allows any extra properties to be defined in an action. + [extraProps: string]: any +} + +/* action creators */ + +/** + * An *action creator* is, quite simply, a function that creates an action. Do + * not confuse the two terms—again, an action is a payload of information, and + * an action creator is a factory that creates an action. + * + * Calling an action creator only produces an action, but does not dispatch + * it. You need to call the store's `dispatch` function to actually cause the + * mutation. Sometimes we say *bound action creators* to mean functions that + * call an action creator and immediately dispatch its result to a specific + * store instance. + * + * If an action creator needs to read the current state, perform an API call, + * or cause a side effect, like a routing transition, it should return an + * async action instead of an action. + * + * @template A Returned action type. + */ +export interface ActionCreator { + (...args: any[]): A +} + +/** + * Object whose values are action creator functions. + */ +export interface ActionCreatorsMapObject { + [key: string]: ActionCreator +} diff --git a/src/types/middleware.ts b/src/types/middleware.ts new file mode 100644 index 0000000000..d631d988da --- /dev/null +++ b/src/types/middleware.ts @@ -0,0 +1,31 @@ +import { Dispatch } from './store' +import { AnyAction } from './actions' + +export interface MiddlewareAPI { + dispatch: D + getState(): S +} + +/** + * A middleware is a higher-order function that composes a dispatch function + * to return a new dispatch function. It often turns async actions into + * actions. + * + * Middleware is composable using function composition. It is useful for + * logging actions, performing side effects like routing, or turning an + * asynchronous API call into a series of synchronous actions. + * + * @template DispatchExt Extra Dispatch signature added by this middleware. + * @template S The type of the state supported by this middleware. + * @template D The type of Dispatch of the store where this middleware is + * installed. + */ +export interface Middleware< + _DispatchExt = {}, // TODO: remove unused component (breaking change) + S = any, + D extends Dispatch = Dispatch +> { + (api: MiddlewareAPI): ( + next: Dispatch + ) => (action: any) => any +} diff --git a/src/types/reducers.ts b/src/types/reducers.ts new file mode 100644 index 0000000000..d0c41a9d33 --- /dev/null +++ b/src/types/reducers.ts @@ -0,0 +1,85 @@ +import { Action, AnyAction } from './actions' + +/* reducers */ + +/** + * A *reducer* (also called a *reducing function*) is a function that accepts + * an accumulation and a value and returns a new accumulation. They are used + * to reduce a collection of values down to a single value + * + * Reducers are not unique to Redux—they are a fundamental concept in + * functional programming. Even most non-functional languages, like + * JavaScript, have a built-in API for reducing. In JavaScript, it's + * `Array.prototype.reduce()`. + * + * In Redux, the accumulated value is the state object, and the values being + * accumulated are actions. Reducers calculate a new state given the previous + * state and an action. They must be *pure functions*—functions that return + * the exact same output for given inputs. They should also be free of + * side-effects. This is what enables exciting features like hot reloading and + * time travel. + * + * Reducers are the most important concept in Redux. + * + * *Do not put API calls into reducers.* + * + * @template S The type of state consumed and produced by this reducer. + * @template A The type of actions the reducer can potentially respond to. + */ +export type Reducer = ( + state: S | undefined, + action: A +) => S + +/** + * Object whose values correspond to different reducer functions. + * + * @template A The type of actions the reducers can potentially respond to. + */ +export type ReducersMapObject = { + [K in keyof S]: Reducer +} + +/** + * Infer a combined state shape from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type StateFromReducersMapObject = M extends ReducersMapObject< + any, + any +> + ? { [P in keyof M]: M[P] extends Reducer ? S : never } + : never + +/** + * Infer reducer union type from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type ReducerFromReducersMapObject = M extends { + [P in keyof M]: infer R +} + ? R extends Reducer + ? R + : never + : never + +/** + * Infer action type from a reducer function. + * + * @template R Type of reducer. + */ +export type ActionFromReducer = R extends Reducer ? A : never + +/** + * Infer action union type from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type ActionFromReducersMapObject = M extends ReducersMapObject< + any, + any +> + ? ActionFromReducer> + : never diff --git a/src/types/store.ts b/src/types/store.ts new file mode 100644 index 0000000000..aaebb0ad21 --- /dev/null +++ b/src/types/store.ts @@ -0,0 +1,269 @@ +/// +import { Action, AnyAction } from './actions' +import { Reducer } from './reducers' + +/** + * Extend the state + * + * This is used by store enhancers and store creators to extend state. + * If there is no state extension, it just returns the state, as is, otherwise + * it returns the state joined with its extension. + * + * Reference for future devs: + * https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919 + */ +export type ExtendState = [Extension] extends [never] + ? State + : State & Extension + +/** + * Internal "virtual" symbol used to make the `CombinedState` type unique. + */ +declare const $CombinedState: unique symbol + +/** + * State base type for reducers created with `combineReducers()`. + * + * This type allows the `createStore()` method to infer which levels of the + * preloaded state can be partial. + * + * Because Typescript is really duck-typed, a type needs to have some + * identifying property to differentiate it from other types with matching + * prototypes for type checking purposes. That's why this type has the + * `$CombinedState` symbol property. Without the property, this type would + * match any object. The symbol doesn't really exist because it's an internal + * (i.e. not exported), and internally we never check its value. Since it's a + * symbol property, it's not expected to be unumerable, and the value is + * typed as always undefined, so its never expected to have a meaningful + * value anyway. It just makes this type distinquishable from plain `{}`. + */ +export type CombinedState = { readonly [$CombinedState]?: undefined } & S + +/** + * Recursively makes combined state objects partial. Only combined state _root + * objects_ (i.e. the generated higher level object with keys mapping to + * individual reducers) are partial. + */ +export type PreloadedState = Required extends { + [$CombinedState]: undefined +} + ? S extends CombinedState + ? { + [K in keyof S1]?: S1[K] extends object ? PreloadedState : S1[K] + } + : never + : { + [K in keyof S]: S[K] extends object ? PreloadedState : S[K] + } + +/** + * A *dispatching function* (or simply *dispatch function*) is a function that + * accepts an action or an async action; it then may or may not dispatch one + * or more actions to the store. + * + * We must distinguish between dispatching functions in general and the base + * `dispatch` function provided by the store instance without any middleware. + * + * The base dispatch function *always* synchronously sends an action to the + * store's reducer, along with the previous state returned by the store, to + * calculate a new state. It expects actions to be plain objects ready to be + * consumed by the reducer. + * + * Middleware wraps the base dispatch function. It allows the dispatch + * function to handle async actions in addition to actions. Middleware may + * transform, delay, ignore, or otherwise interpret actions or async actions + * before passing them to the next middleware. + * + * @template A The type of things (actions or otherwise) which may be + * dispatched. + */ +export interface Dispatch { + (action: T, ...extraArgs: any[]): T +} + +/** + * Function to remove listener added by `Store.subscribe()`. + */ +export interface Unsubscribe { + (): void +} + +/** + * A minimal observable of state changes. + * For more information, see the observable proposal: + * https://github.com/tc39/proposal-observable + */ +export type Observable = { + /** + * The minimal observable subscription method. + * @param {Object} observer Any object that can be used as an observer. + * The observer object should have a `next` method. + * @returns {subscription} An object with an `unsubscribe` method that can + * be used to unsubscribe the observable from the store, and prevent further + * emission of values from the observable. + */ + subscribe: (observer: Observer) => { unsubscribe: Unsubscribe } + [Symbol.observable](): Observable +} + +/** + * An Observer is used to receive data from an Observable, and is supplied as + * an argument to subscribe. + */ +export type Observer = { + next?(value: T): void +} + +/** + * A store is an object that holds the application's state tree. + * There should only be a single store in a Redux app, as the composition + * happens on the reducer level. + * + * @template S The type of state held by this store. + * @template A the type of actions which may be dispatched by this store. + * @template StateExt any extension to state from store enhancers + * @template Ext any extensions to the store from store enhancers + */ +export interface Store< + S = any, + A extends Action = AnyAction, + StateExt = never, + Ext = {} +> { + /** + * Dispatches an action. It is the only way to trigger a state change. + * + * The `reducer` function, used to create the store, will be called with the + * current state tree and the given `action`. Its return value will be + * considered the **next** state of the tree, and the change listeners will + * be notified. + * + * The base implementation only supports plain object actions. If you want + * to dispatch a Promise, an Observable, a thunk, or something else, you + * need to wrap your store creating function into the corresponding + * middleware. For example, see the documentation for the `redux-thunk` + * package. Even the middleware will eventually dispatch plain object + * actions using this method. + * + * @param action A plain object representing “what changed”. It is a good + * idea to keep actions serializable so you can record and replay user + * sessions, or use the time travelling `redux-devtools`. An action must + * have a `type` property which may not be `undefined`. It is a good idea + * to use string constants for action types. + * + * @returns For convenience, the same action object you dispatched. + * + * Note that, if you use a custom middleware, it may wrap `dispatch()` to + * return something else (for example, a Promise you can await). + */ + dispatch: Dispatch + + /** + * Reads the state tree managed by the store. + * + * @returns The current state tree of your application. + */ + getState(): S + + /** + * Adds a change listener. It will be called any time an action is + * dispatched, and some part of the state tree may potentially have changed. + * You may then call `getState()` to read the current state tree inside the + * callback. + * + * You may call `dispatch()` from a change listener, with the following + * caveats: + * + * 1. The subscriptions are snapshotted just before every `dispatch()` call. + * If you subscribe or unsubscribe while the listeners are being invoked, + * this will not have any effect on the `dispatch()` that is currently in + * progress. However, the next `dispatch()` call, whether nested or not, + * will use a more recent snapshot of the subscription list. + * + * 2. The listener should not expect to see all states changes, as the state + * might have been updated multiple times during a nested `dispatch()` before + * the listener is called. It is, however, guaranteed that all subscribers + * registered before the `dispatch()` started will be called with the latest + * state by the time it exits. + * + * @param listener A callback to be invoked on every dispatch. + * @returns A function to remove this change listener. + */ + subscribe(listener: () => void): Unsubscribe + + /** + * Replaces the reducer currently used by the store to calculate the state. + * + * You might need this if your app implements code splitting and you want to + * load some of the reducers dynamically. You might also need this if you + * implement a hot reloading mechanism for Redux. + * + * @param nextReducer The reducer for the store to use instead. + */ + replaceReducer( + nextReducer: Reducer + ): Store, NewActions, StateExt, Ext> & Ext + + /** + * Interoperability point for observable/reactive libraries. + * @returns {observable} A minimal observable of state changes. + * For more information, see the observable proposal: + * https://github.com/tc39/proposal-observable + */ + [Symbol.observable](): Observable +} + +/** + * A store creator is a function that creates a Redux store. Like with + * dispatching function, we must distinguish the base store creator, + * `createStore(reducer, preloadedState)` exported from the Redux package, from + * store creators that are returned from the store enhancers. + * + * @template S The type of state to be held by the store. + * @template A The type of actions which may be dispatched. + * @template Ext Store extension that is mixed in to the Store type. + * @template StateExt State extension that is mixed into the state type. + */ +export interface StoreCreator { + ( + reducer: Reducer, + enhancer?: StoreEnhancer + ): Store, A, StateExt, Ext> & Ext + ( + reducer: Reducer, + preloadedState?: PreloadedState, + enhancer?: StoreEnhancer + ): Store, A, StateExt, Ext> & Ext +} + +/** + * A store enhancer is a higher-order function that composes a store creator + * to return a new, enhanced store creator. This is similar to middleware in + * that it allows you to alter the store interface in a composable way. + * + * Store enhancers are much the same concept as higher-order components in + * React, which are also occasionally called “component enhancers”. + * + * Because a store is not an instance, but rather a plain-object collection of + * functions, copies can be easily created and modified without mutating the + * original store. There is an example in `compose` documentation + * demonstrating that. + * + * Most likely you'll never write a store enhancer, but you may use the one + * provided by the developer tools. It is what makes time travel possible + * without the app being aware it is happening. Amusingly, the Redux + * middleware implementation is itself a store enhancer. + * + * @template Ext Store extension that is mixed into the Store type. + * @template StateExt State extension that is mixed into the state type. + */ +export type StoreEnhancer = ( + next: StoreEnhancerStoreCreator +) => StoreEnhancerStoreCreator +export type StoreEnhancerStoreCreator = < + S = any, + A extends Action = AnyAction +>( + reducer: Reducer, + preloadedState?: PreloadedState +) => Store, A, StateExt, Ext> & Ext diff --git a/src/utils/actionTypes.js b/src/utils/actionTypes.ts similarity index 100% rename from src/utils/actionTypes.js rename to src/utils/actionTypes.ts diff --git a/src/utils/isPlainObject.js b/src/utils/isPlainObject.ts similarity index 58% rename from src/utils/isPlainObject.js rename to src/utils/isPlainObject.ts index 15ce724283..9d1956328a 100644 --- a/src/utils/isPlainObject.js +++ b/src/utils/isPlainObject.ts @@ -1,8 +1,8 @@ /** - * @param {any} obj The object to inspect. - * @returns {boolean} True if the argument appears to be a plain object. + * @param obj The object to inspect. + * @returns True if the argument appears to be a plain object. */ -export default function isPlainObject(obj) { +export default function isPlainObject(obj: any): boolean { if (typeof obj !== 'object' || obj === null) return false let proto = obj diff --git a/src/utils/warning.js b/src/utils/warning.ts similarity index 81% rename from src/utils/warning.js rename to src/utils/warning.ts index d35e99b726..cefc5d1da6 100644 --- a/src/utils/warning.js +++ b/src/utils/warning.ts @@ -1,10 +1,9 @@ /** * Prints a warning in the console if it exists. * - * @param {String} message The warning message. - * @returns {void} + * @param message The warning message. */ -export default function warning(message) { +export default function warning(message: string): void { /* eslint-disable no-console */ if (typeof console !== 'undefined' && typeof console.error === 'function') { console.error(message) diff --git a/test/applyMiddleware.spec.js b/test/applyMiddleware.spec.ts similarity index 77% rename from test/applyMiddleware.spec.js rename to test/applyMiddleware.spec.ts index 2efdc9f83f..fefd849b15 100644 --- a/test/applyMiddleware.spec.js +++ b/test/applyMiddleware.spec.ts @@ -1,4 +1,4 @@ -import { createStore, applyMiddleware } from '../' +import { createStore, applyMiddleware, Middleware, AnyAction, Action } from '..' import * as reducers from './helpers/reducers' import { addTodo, addTodoAsync, addTodoIfEmpty } from './helpers/actionCreators' import { thunk } from './helpers/middleware' @@ -51,7 +51,11 @@ describe('applyMiddleware', () => { const spy = jest.fn() const store = applyMiddleware(test(spy), thunk)(createStore)(reducers.todos) - return store.dispatch(addTodoAsync('Use Redux')).then(() => { + // the typing for redux-thunk is super complex, so we will use an as unknown hack + const dispatchedValue = (store.dispatch( + addTodoAsync('Use Redux') + ) as unknown) as Promise + return dispatchedValue.then(() => { expect(spy.mock.calls.length).toEqual(2) }) }) @@ -87,7 +91,11 @@ describe('applyMiddleware', () => { } ]) - store.dispatch(addTodoAsync('Maybe')).then(() => { + // the typing for redux-thunk is super complex, so we will use an "as unknown" hack + const dispatchedValue = (store.dispatch( + addTodoAsync('Maybe') + ) as unknown) as Promise + dispatchedValue.then(() => { expect(store.getState()).toEqual([ { id: 1, @@ -110,8 +118,16 @@ describe('applyMiddleware', () => { const spy = jest.fn() const testCallArgs = ['test'] - function multiArgMiddleware() { - return next => (action, callArgs) => { + interface MultiDispatch { + (action: T, extraArg?: string[]): T + } + + const multiArgMiddleware: Middleware< + MultiDispatch, + any, + MultiDispatch + > = _store => { + return next => (action, callArgs?: any) => { if (Array.isArray(callArgs)) { return action(...callArgs) } @@ -120,7 +136,7 @@ describe('applyMiddleware', () => { } function dummyMiddleware({ dispatch }) { - return next => action => dispatch(action, testCallArgs) + return _next => action => dispatch(action, testCallArgs) } const store = createStore( diff --git a/test/bindActionCreators.spec.js b/test/bindActionCreators.spec.ts similarity index 88% rename from test/bindActionCreators.spec.js rename to test/bindActionCreators.spec.ts index a9e2f2a6ea..5efecb6902 100644 --- a/test/bindActionCreators.spec.js +++ b/test/bindActionCreators.spec.ts @@ -1,4 +1,4 @@ -import { bindActionCreators, createStore } from '../' +import { bindActionCreators, createStore, ActionCreator } from '..' import { todos } from './helpers/reducers' import * as actionCreators from './helpers/actionCreators' @@ -46,15 +46,17 @@ describe('bindActionCreators', () => { }) it('skips non-function values in the passed object', () => { + // as this is testing against invalid values, we will cast to unknown and then back to ActionCreator + // in a typescript environment this test is unnecessary, but required in javascript const boundActionCreators = bindActionCreators( - { + ({ ...actionCreators, foo: 42, bar: 'baz', wow: undefined, much: {}, test: null - }, + } as unknown) as ActionCreator, store.dispatch ) expect(Object.keys(boundActionCreators)).toEqual( @@ -91,7 +93,10 @@ describe('bindActionCreators', () => { it('throws for a primitive actionCreator', () => { expect(() => { - bindActionCreators('string', store.dispatch) + bindActionCreators( + ('string' as unknown) as ActionCreator, + store.dispatch + ) }).toThrow( 'bindActionCreators expected an object or a function, instead received string. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?' diff --git a/test/combineReducers.spec.js b/test/combineReducers.spec.ts similarity index 74% rename from test/combineReducers.spec.js rename to test/combineReducers.spec.ts index e0e69aa110..161e00658b 100644 --- a/test/combineReducers.spec.js +++ b/test/combineReducers.spec.ts @@ -2,34 +2,40 @@ import { createStore, combineReducers, - __DO_NOT_USE__ActionTypes as ActionTypes -} from '../' + Reducer, + AnyAction, + __DO_NOT_USE__ActionTypes as ActionTypes, + CombinedState +} from '..' describe('Utils', () => { describe('combineReducers', () => { it('returns a composite reducer that maps the state keys to given reducers', () => { const reducer = combineReducers({ - counter: (state = 0, action) => + counter: (state: number = 0, action) => action.type === 'increment' ? state + 1 : state, - stack: (state = [], action) => + stack: (state: any[] = [], action) => action.type === 'push' ? [...state, action.value] : state }) - const s1 = reducer({}, { type: 'increment' }) + const s1 = reducer(undefined, { type: 'increment' }) expect(s1).toEqual({ counter: 1, stack: [] }) const s2 = reducer(s1, { type: 'push', value: 'a' }) expect(s2).toEqual({ counter: 1, stack: ['a'] }) }) it('ignores all props which are not a function', () => { + // we double-cast because these conditions can only happen in a javascript setting const reducer = combineReducers({ - fake: true, - broken: 'string', - another: { nested: 'object' }, + fake: (true as unknown) as Reducer, + broken: ('string' as unknown) as Reducer, + another: ({ nested: 'object' } as unknown) as Reducer, stack: (state = []) => state }) - expect(Object.keys(reducer({}, { type: 'push' }))).toEqual(['stack']) + expect(Object.keys(reducer(undefined, { type: 'push' }))).toEqual([ + 'stack' + ]) }) it('warns if a reducer prop is undefined', () => { @@ -55,7 +61,7 @@ describe('Utils', () => { it('throws an error if a reducer returns undefined handling an action', () => { const reducer = combineReducers({ - counter(state = 0, action) { + counter(state: number = 0, action) { switch (action && action.type) { case 'increment': return state + 1 @@ -77,12 +83,14 @@ describe('Utils', () => { expect(() => reducer({ counter: 0 }, null)).toThrow( /"counter".*an action/ ) - expect(() => reducer({ counter: 0 }, {})).toThrow(/"counter".*an action/) + expect(() => + reducer({ counter: 0 }, ({} as unknown) as AnyAction) + ).toThrow(/"counter".*an action/) }) it('throws an error on first call if a reducer returns undefined initializing', () => { const reducer = combineReducers({ - counter(state, action) { + counter(state: number, action) { switch (action.type) { case 'increment': return state + 1 @@ -93,7 +101,9 @@ describe('Utils', () => { } } }) - expect(() => reducer({})).toThrow(/"counter".*initialization/) + expect(() => reducer(undefined, { type: '' })).toThrow( + /"counter".*initialization/ + ) }) it('catches error thrown in reducer when initializing and re-throw', () => { @@ -102,14 +112,16 @@ describe('Utils', () => { throw new Error('Error thrown in reducer') } }) - expect(() => reducer({})).toThrow(/Error thrown in reducer/) + expect(() => + reducer(undefined, (undefined as unknown) as AnyAction) + ).toThrow(/Error thrown in reducer/) }) it('allows a symbol to be used as an action type', () => { const increment = Symbol('INCREMENT') const reducer = combineReducers({ - counter(state = 0, action) { + counter(state: number = 0, action) { switch (action.type) { case increment: return state + 1 @@ -135,7 +147,7 @@ describe('Utils', () => { } }) - const initialState = reducer(undefined, '@@INIT') + const initialState = reducer(undefined, { type: '@@INIT' }) expect(reducer(initialState, { type: 'FOO' })).toBe(initialState) }) @@ -144,7 +156,7 @@ describe('Utils', () => { child1(state = {}) { return state }, - child2(state = { count: 0 }, action) { + child2(state: { count: number } = { count: 0 }, action) { switch (action.type) { case 'increment': return { count: state.count + 1 } @@ -157,7 +169,7 @@ describe('Utils', () => { } }) - const initialState = reducer(undefined, '@@INIT') + const initialState = reducer(undefined, { type: '@@INIT' }) expect(reducer(initialState, { type: 'increment' })).not.toBe( initialState ) @@ -165,7 +177,7 @@ describe('Utils', () => { it('throws an error on first call if a reducer attempts to handle a private action', () => { const reducer = combineReducers({ - counter(state, action) { + counter(state: number, action) { switch (action.type) { case 'increment': return state + 1 @@ -179,7 +191,9 @@ describe('Utils', () => { } } }) - expect(() => reducer()).toThrow(/"counter".*private/) + expect(() => + reducer(undefined, (undefined as unknown) as AnyAction) + ).toThrow(/"counter".*private/) }) it('warns if no reducers are passed to combineReducers', () => { @@ -188,7 +202,7 @@ describe('Utils', () => { console.error = spy const reducer = combineReducers({}) - reducer({}) + reducer(undefined, { type: '' }) expect(spy.mock.calls[0][0]).toMatch( /Store does not have a valid reducer/ ) @@ -200,9 +214,17 @@ describe('Utils', () => { it('warns if input state does not match reducer shape', () => { const preSpy = console.error const spy = jest.fn() + const nullAction = (undefined as unknown) as AnyAction console.error = spy - const reducer = combineReducers({ + interface ShapeState { + foo: { bar: number } + baz: { qux: number } + } + + type ShapeMismatchState = CombinedState + + const reducer = combineReducers({ foo(state = { bar: 1 }) { return state }, @@ -211,44 +233,51 @@ describe('Utils', () => { } }) - reducer() + reducer(undefined, nullAction) expect(spy.mock.calls.length).toBe(0) - reducer({ foo: { bar: 2 } }) + reducer(({ foo: { bar: 2 } } as unknown) as ShapeState, nullAction) expect(spy.mock.calls.length).toBe(0) - reducer({ - foo: { bar: 2 }, - baz: { qux: 4 } - }) + reducer( + { + foo: { bar: 2 }, + baz: { qux: 4 } + }, + nullAction + ) expect(spy.mock.calls.length).toBe(0) - createStore(reducer, { bar: 2 }) + createStore(reducer, ({ bar: 2 } as unknown) as ShapeState) expect(spy.mock.calls[0][0]).toMatch( /Unexpected key "bar".*createStore.*instead: "foo", "baz"/ ) - createStore(reducer, { bar: 2, qux: 4, thud: 5 }) + createStore(reducer, ({ + bar: 2, + qux: 4, + thud: 5 + } as unknown) as ShapeState) expect(spy.mock.calls[1][0]).toMatch( /Unexpected keys "qux", "thud".*createStore.*instead: "foo", "baz"/ ) - createStore(reducer, 1) + createStore(reducer, (1 as unknown) as ShapeState) expect(spy.mock.calls[2][0]).toMatch( /createStore has unexpected type of "Number".*keys: "foo", "baz"/ ) - reducer({ corge: 2 }) + reducer(({ corge: 2 } as unknown) as ShapeState, nullAction) expect(spy.mock.calls[3][0]).toMatch( /Unexpected key "corge".*reducer.*instead: "foo", "baz"/ ) - reducer({ fred: 2, grault: 4 }) + reducer(({ fred: 2, grault: 4 } as unknown) as ShapeState, nullAction) expect(spy.mock.calls[4][0]).toMatch( /Unexpected keys "fred", "grault".*reducer.*instead: "foo", "baz"/ ) - reducer(1) + reducer((1 as unknown) as ShapeState, nullAction) expect(spy.mock.calls[5][0]).toMatch( /reducer has unexpected type of "Number".*keys: "foo", "baz"/ ) @@ -261,25 +290,32 @@ describe('Utils', () => { const preSpy = console.error const spy = jest.fn() console.error = spy + const nullAction = { type: '' } const foo = (state = { foo: 1 }) => state const bar = (state = { bar: 2 }) => state expect(spy.mock.calls.length).toBe(0) + interface WarnState { + foo: { foo: number } + bar: { bar: number } + } + const reducer = combineReducers({ foo, bar }) - const state = { foo: 1, bar: 2, qux: 3 } + const state = ({ foo: 1, bar: 2, qux: 3 } as unknown) as WarnState + const bazState = ({ ...state, baz: 5 } as unknown) as WarnState - reducer(state, {}) - reducer(state, {}) - reducer(state, {}) - reducer(state, {}) + reducer(state, nullAction) + reducer(state, nullAction) + reducer(state, nullAction) + reducer(state, nullAction) expect(spy.mock.calls.length).toBe(1) - reducer({ ...state, baz: 5 }, {}) - reducer({ ...state, baz: 5 }, {}) - reducer({ ...state, baz: 5 }, {}) - reducer({ ...state, baz: 5 }, {}) + reducer(bazState, nullAction) + reducer({ ...bazState }, nullAction) + reducer({ ...bazState }, nullAction) + reducer({ ...bazState }, nullAction) expect(spy.mock.calls.length).toBe(2) spy.mockClear() diff --git a/test/compose.spec.js b/test/compose.spec.ts similarity index 66% rename from test/compose.spec.js rename to test/compose.spec.ts index 392b403f98..d25131cdeb 100644 --- a/test/compose.spec.js +++ b/test/compose.spec.ts @@ -1,10 +1,10 @@ -import { compose } from '../' +import { compose } from '..' describe('Utils', () => { describe('compose', () => { it('composes from right to left', () => { - const double = x => x * 2 - const square = x => x * x + const double = (x: number) => x * 2 + const square = (x: number) => x * x expect(compose(square)(5)).toBe(25) expect( compose( @@ -22,10 +22,10 @@ describe('Utils', () => { }) it('composes functions from right to left', () => { - const a = next => x => next(x + 'a') - const b = next => x => next(x + 'b') - const c = next => x => next(x + 'c') - const final = x => x + const a = (next: (x: string) => string) => (x: string) => next(x + 'a') + const b = (next: (x: string) => string) => (x: string) => next(x + 'b') + const c = (next: (x: string) => string) => (x: string) => next(x + 'c') + const final = (x: string) => x expect( compose( @@ -51,14 +51,15 @@ describe('Utils', () => { }) it('throws at runtime if argument is not a function', () => { - const square = x => x * x - const add = (x, y) => x + y + type sFunc = (x: number, y: number) => number + const square = (x: number, _: number) => x * x + const add = (x: number, y: number) => x + y expect(() => compose( square, add, - false + (false as unknown) as sFunc )(1, 2) ).toThrow() expect(() => @@ -72,28 +73,28 @@ describe('Utils', () => { compose( square, add, - true + (true as unknown) as sFunc )(1, 2) ).toThrow() expect(() => compose( square, add, - NaN + (NaN as unknown) as sFunc )(1, 2) ).toThrow() expect(() => compose( square, add, - '42' + ('42' as unknown) as sFunc )(1, 2) ).toThrow() }) it('can be seeded with multiple arguments', () => { - const square = x => x * x - const add = (x, y) => x + y + const square = (x: number, _: number) => x * x + const add = (x: number, y: number) => x + y expect( compose( square, @@ -103,9 +104,9 @@ describe('Utils', () => { }) it('returns the first given argument if given no functions', () => { - expect(compose()(1, 2)).toBe(1) + expect(compose()(1, 2)).toBe(1) expect(compose()(3)).toBe(3) - expect(compose()()).toBe(undefined) + expect(compose()(undefined)).toBe(undefined) }) it('returns the first function if given only one', () => { diff --git a/test/createStore.spec.js b/test/createStore.spec.ts similarity index 90% rename from test/createStore.spec.js rename to test/createStore.spec.ts index 6a837c0d1d..5e8ef1db50 100644 --- a/test/createStore.spec.js +++ b/test/createStore.spec.ts @@ -1,4 +1,11 @@ -import { createStore, combineReducers } from '../' +import { + createStore, + combineReducers, + Reducer, + StoreEnhancer, + Action, + Store +} from '..' import { addTodo, dispatchInMiddle, @@ -9,7 +16,7 @@ import { unknownAction } from './helpers/actionCreators' import * as reducers from './helpers/reducers' -import { from } from 'rxjs' +import { from, ObservableInput } from 'rxjs' import { map } from 'rxjs/operators' import $$observable from 'symbol-observable' @@ -26,11 +33,11 @@ describe('createStore', () => { }) it('throws if reducer is not a function', () => { - expect(() => createStore()).toThrow() + expect(() => createStore(undefined)).toThrow() - expect(() => createStore('test')).toThrow() + expect(() => createStore(('test' as unknown) as Reducer)).toThrow() - expect(() => createStore({})).toThrow() + expect(() => createStore(({} as unknown) as Reducer)).toThrow() expect(() => createStore(() => {})).not.toThrow() }) @@ -568,13 +575,23 @@ describe('createStore', () => { }) it('throws if enhancer is neither undefined nor a function', () => { - expect(() => createStore(reducers.todos, undefined, {})).toThrow() + expect(() => + createStore(reducers.todos, undefined, ({} as unknown) as StoreEnhancer) + ).toThrow() - expect(() => createStore(reducers.todos, undefined, [])).toThrow() + expect(() => + createStore(reducers.todos, undefined, ([] as unknown) as StoreEnhancer) + ).toThrow() expect(() => createStore(reducers.todos, undefined, null)).toThrow() - expect(() => createStore(reducers.todos, undefined, false)).toThrow() + expect(() => + createStore( + reducers.todos, + undefined, + (false as unknown) as StoreEnhancer + ) + ).toThrow() expect(() => createStore(reducers.todos, undefined, undefined) @@ -586,25 +603,27 @@ describe('createStore', () => { expect(() => createStore(reducers.todos, [])).not.toThrow() - expect(() => createStore(reducers.todos, {})).not.toThrow() + expect(() => + createStore<{}, Action, {}, {}>(reducers.todos, {}) + ).not.toThrow() }) it('throws if nextReducer is not a function', () => { const store = createStore(reducers.todos) - expect(() => store.replaceReducer()).toThrow( + expect(() => store.replaceReducer(undefined)).toThrow( 'Expected the nextReducer to be a function.' ) - expect(() => store.replaceReducer(() => {})).not.toThrow() + expect(() => store.replaceReducer(() => [])).not.toThrow() }) it('throws if listener is not a function', () => { const store = createStore(reducers.todos) - expect(() => store.subscribe()).toThrow() + expect(() => store.subscribe(undefined)).toThrow() - expect(() => store.subscribe('')).toThrow() + expect(() => store.subscribe(('' as unknown) as () => void)).toThrow() expect(() => store.subscribe(null)).toThrow() @@ -654,11 +673,11 @@ describe('createStore', () => { }) it('should pass an integration test with no unsubscribe', () => { - function foo(state = 0, action) { + function foo(state = 0, action: Action) { return action.type === 'foo' ? 1 : state } - function bar(state = 0, action) { + function bar(state = 0, action: Action) { return action.type === 'bar' ? 2 : state } @@ -667,7 +686,7 @@ describe('createStore', () => { const results = [] observable.subscribe({ - next(state) { + next(state: any) { results.push(state) } }) @@ -683,11 +702,11 @@ describe('createStore', () => { }) it('should pass an integration test with an unsubscribe', () => { - function foo(state = 0, action) { + function foo(state = 0, action: Action) { return action.type === 'foo' ? 1 : state } - function bar(state = 0, action) { + function bar(state = 0, action: Action) { return action.type === 'bar' ? 2 : state } @@ -696,7 +715,7 @@ describe('createStore', () => { const results = [] const sub = observable.subscribe({ - next(state) { + next(state: any) { results.push(state) } }) @@ -709,25 +728,26 @@ describe('createStore', () => { }) it('should pass an integration test with a common library (RxJS)', () => { - function foo(state = 0, action) { + function foo(state = 0, action: Action) { return action.type === 'foo' ? 1 : state } - function bar(state = 0, action) { + function bar(state = 0, action: Action) { return action.type === 'bar' ? 2 : state } - const store = createStore(combineReducers({ foo, bar })) + const store: ObservableInput<{ foo: number; bar: number }> = createStore( + combineReducers({ foo, bar }) + ) const observable = from(store) const results = [] const sub = observable .pipe(map(state => ({ fromRx: true, ...state }))) .subscribe(state => results.push(state)) - - store.dispatch({ type: 'foo' }) + ;(store as Store).dispatch({ type: 'foo' }) sub.unsubscribe() - store.dispatch({ type: 'bar' }) + ;(store as Store).dispatch({ type: 'bar' }) expect(results).toEqual([ { foo: 0, bar: 0, fromRx: true }, @@ -742,10 +762,10 @@ describe('createStore', () => { const store = createStore( combineReducers({ - x: (s = 0, a) => s, + x: (s = 0, _) => s, y: combineReducers({ - z: (s = 0, a) => s, - w: (s = 0, a) => s + z: (s = 0, _) => s, + w: (s = 0, _) => s }) }) ) @@ -753,12 +773,12 @@ describe('createStore', () => { store.replaceReducer( combineReducers({ y: combineReducers({ - z: (s = 0, a) => s + z: (s = 0, _) => s }) }) ) - expect(console.error.mock.calls.length).toBe(0) + expect((console.error as any).mock.calls.length).toBe(0) console.error = originalConsoleError }) @@ -766,15 +786,22 @@ describe('createStore', () => { const rootReducer = combineReducers(reducers) const dummyEnhancer = f => f expect(() => - createStore(rootReducer, dummyEnhancer, dummyEnhancer) + // use a fake pre-loaded state to get a valid createStore signature + createStore(rootReducer, (dummyEnhancer as unknown) as {}, dummyEnhancer) ).toThrow() }) it('throws if passing several enhancer functions with preloaded state', () => { const rootReducer = combineReducers(reducers) - const dummyEnhancer = f => f + const dummyEnhancer = (f: any) => f expect(() => - createStore(rootReducer, { todos: [] }, dummyEnhancer, dummyEnhancer) + // work around the type so we can call this poorly + (createStore as any)( + rootReducer, + { todos: [] }, + dummyEnhancer, + dummyEnhancer + ) ).toThrow() }) }) diff --git a/test/helpers/actionCreators.js b/test/helpers/actionCreators.ts similarity index 52% rename from test/helpers/actionCreators.js rename to test/helpers/actionCreators.ts index c75a7c3084..46964ea9ce 100644 --- a/test/helpers/actionCreators.js +++ b/test/helpers/actionCreators.ts @@ -7,13 +7,14 @@ import { THROW_ERROR, UNKNOWN_ACTION } from './actionTypes' +import { Action, AnyAction, Dispatch } from '../..' -export function addTodo(text) { +export function addTodo(text: string): AnyAction { return { type: ADD_TODO, text } } -export function addTodoAsync(text) { - return dispatch => +export function addTodoAsync(text: string) { + return (dispatch: Dispatch): Promise => new Promise(resolve => setImmediate(() => { dispatch(addTodo(text)) @@ -22,49 +23,49 @@ export function addTodoAsync(text) { ) } -export function addTodoIfEmpty(text) { - return (dispatch, getState) => { +export function addTodoIfEmpty(text: string) { + return (dispatch: Dispatch, getState: () => any) => { if (!getState().length) { dispatch(addTodo(text)) } } } -export function dispatchInMiddle(boundDispatchFn) { +export function dispatchInMiddle(boundDispatchFn: () => void): AnyAction { return { type: DISPATCH_IN_MIDDLE, boundDispatchFn } } -export function getStateInMiddle(boundGetStateFn) { +export function getStateInMiddle(boundGetStateFn: () => void): AnyAction { return { type: GET_STATE_IN_MIDDLE, boundGetStateFn } } -export function subscribeInMiddle(boundSubscribeFn) { +export function subscribeInMiddle(boundSubscribeFn: () => void): AnyAction { return { type: SUBSCRIBE_IN_MIDDLE, boundSubscribeFn } } -export function unsubscribeInMiddle(boundUnsubscribeFn) { +export function unsubscribeInMiddle(boundUnsubscribeFn: () => void): AnyAction { return { type: UNSUBSCRIBE_IN_MIDDLE, boundUnsubscribeFn } } -export function throwError() { +export function throwError(): Action { return { type: THROW_ERROR } } -export function unknownAction() { +export function unknownAction(): Action { return { type: UNKNOWN_ACTION } diff --git a/test/helpers/actionTypes.js b/test/helpers/actionTypes.ts similarity index 100% rename from test/helpers/actionTypes.js rename to test/helpers/actionTypes.ts diff --git a/test/helpers/middleware.js b/test/helpers/middleware.js deleted file mode 100644 index 06f074fffa..0000000000 --- a/test/helpers/middleware.js +++ /dev/null @@ -1,4 +0,0 @@ -export function thunk({ dispatch, getState }) { - return next => action => - typeof action === 'function' ? action(dispatch, getState) : next(action) -} diff --git a/test/helpers/middleware.ts b/test/helpers/middleware.ts new file mode 100644 index 0000000000..5bf51a6754 --- /dev/null +++ b/test/helpers/middleware.ts @@ -0,0 +1,12 @@ +import { MiddlewareAPI, Dispatch, AnyAction } from '../..' + +type ThunkAction = T extends AnyAction + ? AnyAction + : T extends Function + ? T + : never + +export function thunk({ dispatch, getState }: MiddlewareAPI) { + return (next: Dispatch) => (action: ThunkAction) => + typeof action === 'function' ? action(dispatch, getState) : next(action) +} diff --git a/test/helpers/reducers.js b/test/helpers/reducers.ts similarity index 56% rename from test/helpers/reducers.js rename to test/helpers/reducers.ts index f0030fb2dd..c10d3d017e 100644 --- a/test/helpers/reducers.js +++ b/test/helpers/reducers.ts @@ -6,14 +6,21 @@ import { UNSUBSCRIBE_IN_MIDDLE, THROW_ERROR } from './actionTypes' +import { AnyAction } from '../..' -function id(state = []) { +function id(state: { id: number }[] = []) { return ( state.reduce((result, item) => (item.id > result ? item.id : result), 0) + 1 ) } -export function todos(state = [], action) { +export interface Todo { + id: number + text: string +} +export type TodoAction = { type: 'ADD_TODO'; text: string } | AnyAction + +export function todos(state: Todo[] = [], action: TodoAction) { switch (action.type) { case ADD_TODO: return [ @@ -28,7 +35,7 @@ export function todos(state = [], action) { } } -export function todosReverse(state = [], action) { +export function todosReverse(state: Todo[] = [], action: TodoAction) { switch (action.type) { case ADD_TODO: return [ @@ -43,7 +50,12 @@ export function todosReverse(state = [], action) { } } -export function dispatchInTheMiddleOfReducer(state = [], action) { +export function dispatchInTheMiddleOfReducer( + state = [], + action: + | { type: 'DISPATCH_IN_MIDDLE'; boundDispatchFn: () => void } + | AnyAction +) { switch (action.type) { case DISPATCH_IN_MIDDLE: action.boundDispatchFn() @@ -53,7 +65,12 @@ export function dispatchInTheMiddleOfReducer(state = [], action) { } } -export function getStateInTheMiddleOfReducer(state = [], action) { +export function getStateInTheMiddleOfReducer( + state = [], + action: + | { type: 'DISPATCH_IN_MIDDLE'; boundGetStateFn: () => void } + | AnyAction +) { switch (action.type) { case GET_STATE_IN_MIDDLE: action.boundGetStateFn() @@ -63,7 +80,12 @@ export function getStateInTheMiddleOfReducer(state = [], action) { } } -export function subscribeInTheMiddleOfReducer(state = [], action) { +export function subscribeInTheMiddleOfReducer( + state = [], + action: + | { type: 'DISPATCH_IN_MIDDLE'; boundSubscribeFn: () => void } + | AnyAction +) { switch (action.type) { case SUBSCRIBE_IN_MIDDLE: action.boundSubscribeFn() @@ -73,7 +95,12 @@ export function subscribeInTheMiddleOfReducer(state = [], action) { } } -export function unsubscribeInTheMiddleOfReducer(state = [], action) { +export function unsubscribeInTheMiddleOfReducer( + state = [], + action: + | { type: 'DISPATCH_IN_MIDDLE'; boundUnsubscribeFn: () => void } + | AnyAction +) { switch (action.type) { case UNSUBSCRIBE_IN_MIDDLE: action.boundUnsubscribeFn() @@ -83,7 +110,7 @@ export function unsubscribeInTheMiddleOfReducer(state = [], action) { } } -export function errorThrowingReducer(state = [], action) { +export function errorThrowingReducer(state = [], action: AnyAction) { switch (action.type) { case THROW_ERROR: throw new Error() diff --git a/test/replaceReducers.spec.ts b/test/replaceReducers.spec.ts new file mode 100644 index 0000000000..ed70106b12 --- /dev/null +++ b/test/replaceReducers.spec.ts @@ -0,0 +1,18 @@ +import { createStore, combineReducers } from '..' + +describe('replaceReducers test', () => { + it('returns the original store', () => { + const nextReducer = combineReducers({ + foo: (state = 1, _action) => state, + bar: (state = 2, _action) => state + }) + const store = createStore((state, action) => { + if (state === undefined) return { type: 5 } + return action + }) + + const nextStore = store.replaceReducer(nextReducer) + + expect(nextStore).toBe(store) + }) +}) diff --git a/test/typescript.spec.js b/test/typescript.spec.ts similarity index 100% rename from test/typescript.spec.js rename to test/typescript.spec.ts diff --git a/test/typescript/.eslintrc.js b/test/typescript/.eslintrc.js new file mode 100644 index 0000000000..d94f0a7925 --- /dev/null +++ b/test/typescript/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + rules: { + 'no-unused-expressions': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-unused-vars': 'off' + } +} diff --git a/test/typescript/actionCreators.ts b/test/typescript/actionCreators.ts index e3594a0712..f7a15bcd75 100644 --- a/test/typescript/actionCreators.ts +++ b/test/typescript/actionCreators.ts @@ -4,7 +4,7 @@ import { Dispatch, bindActionCreators, ActionCreatorsMapObject -} from 'redux' +} from '../..' interface AddTodoAction extends Action { text: string diff --git a/test/typescript/actions.ts b/test/typescript/actions.ts index 1b701ce77d..480a34151b 100644 --- a/test/typescript/actions.ts +++ b/test/typescript/actions.ts @@ -1,4 +1,4 @@ -import { Action as ReduxAction } from 'redux' +import { Action as ReduxAction } from '../..' namespace FSA { interface Action

extends ReduxAction { diff --git a/test/typescript/compose.ts b/test/typescript/compose.ts index ae2fc5b32d..cfc608bad3 100644 --- a/test/typescript/compose.ts +++ b/test/typescript/compose.ts @@ -1,4 +1,4 @@ -import { compose } from 'redux' +import { compose } from '../..' // adapted from DefinitelyTyped/compose-function diff --git a/test/typescript/dispatch.ts b/test/typescript/dispatch.ts index 72f8998856..9cb125e37a 100644 --- a/test/typescript/dispatch.ts +++ b/test/typescript/dispatch.ts @@ -1,4 +1,4 @@ -import { Dispatch, AnyAction } from 'redux' +import { Dispatch } from '../..' /** * Default Dispatch type accepts any object with `type` property. diff --git a/test/typescript/enhancers.ts b/test/typescript/enhancers.ts index 7259b4884b..c6d29af6a0 100644 --- a/test/typescript/enhancers.ts +++ b/test/typescript/enhancers.ts @@ -1,5 +1,4 @@ -import { PreloadedState } from '../../index' -import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from 'redux' +import { StoreEnhancer, Action, AnyAction, Reducer, createStore } from '../..' interface State { someField: 'string' @@ -12,7 +11,25 @@ const reducer: Reducer = null as any function dispatchExtension() { type PromiseDispatch = (promise: Promise) => Promise - const enhancer: StoreEnhancer<{ dispatch: PromiseDispatch }> = null as any + const enhancer: StoreEnhancer<{ + dispatch: PromiseDispatch + }> = createStore => ( + reducer: Reducer, + preloadedState?: any + ) => { + const store = createStore(reducer, preloadedState) + return { + ...store, + dispatch: (action: any) => { + if (action.type) { + store.dispatch(action) + } else if (action.then) { + action.then(store.dispatch) + } + return action + } + } + } const store = createStore(reducer, enhancer) @@ -37,10 +54,21 @@ function stateExtension() { A extends Action = AnyAction >( reducer: Reducer, - preloadedState?: PreloadedState + preloadedState?: any ) => { - const wrappedReducer: Reducer = null as any - const wrappedPreloadedState: PreloadedState = null as any + const wrappedReducer: Reducer = (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } + const wrappedPreloadedState = preloadedState + ? { + ...preloadedState, + extraField: 'extra' + } + : undefined return createStore(wrappedReducer, wrappedPreloadedState) } @@ -65,3 +93,198 @@ function extraMethods() { // typings:expect-error store.wrongMethod() } + +/** + * replaceReducer with a store enhancer + */ +function replaceReducerExtender() { + interface ExtraState { + extraField: 'extra' + } + + const enhancer: StoreEnhancer< + { method(): string }, + ExtraState + > = createStore => ( + reducer: Reducer, + preloadedState?: any + ) => { + const wrappedReducer: Reducer = (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } + const wrappedPreloadedState = preloadedState + ? { + ...preloadedState, + extraField: 'extra' + } + : undefined + return createStore(wrappedReducer, wrappedPreloadedState) + } + + const store = createStore(reducer, enhancer) + + const newReducer = ( + state: { test: boolean } = { test: true }, + _: AnyAction + ) => state + + const newStore = store.replaceReducer(newReducer) + newStore.getState().test + newStore.getState().extraField + // typings:expect-error + newStore.getState().wrongField + + const res: string = newStore.method() + // typings:expect-error + newStore.wrongMethod() +} + +function mhelmersonExample() { + interface State { + someField: 'string' + } + + interface ExtraState { + extraField: 'extra' + } + + const reducer: Reducer = null as any + + function stateExtensionExpectedToWork() { + interface ExtraState { + extraField: 'extra' + } + + const enhancer: StoreEnhancer<{}, ExtraState> = createStore => < + S, + A extends Action = AnyAction + >( + reducer: Reducer, + preloadedState?: any + ) => { + const wrappedReducer: Reducer = (state, action) => { + const newState = reducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } + const wrappedPreloadedState = preloadedState + ? { + ...preloadedState, + extraField: 'extra' + } + : undefined + const store = createStore(wrappedReducer, wrappedPreloadedState) + return { + ...store, + replaceReducer( + nextReducer: ( + state: NS & ExtraState | undefined, + action: NA + ) => NS & ExtraState + ) { + const nextWrappedReducer: Reducer = ( + state, + action + ) => { + const newState = nextReducer(state, action) + return { + ...newState, + extraField: 'extra' + } + } + return store.replaceReducer(nextWrappedReducer) + } + } + } + + const store = createStore(reducer, enhancer) + store.replaceReducer(reducer) + + store.getState().extraField + // typings:expect-error + store.getState().wrongField + // typings:expect-error + store.getState().test + + const newReducer = ( + state: { test: boolean } = { test: true }, + _: AnyAction + ) => state + + const newStore = store.replaceReducer(newReducer) + newStore.getState().test + newStore.getState().extraField + // typings:expect-error + newStore.getState().wrongField + } +} + +function finalHelmersonExample() { + interface ExtraState { + foo: string + } + + function persistReducer( + config: any, + reducer: Reducer + ) { + return (state: S & ExtraState | undefined, action: AnyAction) => { + const newState = reducer(state, (action as unknown) as A) + return { + ...newState, + foo: 'hi' + } + } + } + + function persistStore(store: S) { + return store + } + + function createPersistEnhancer( + persistConfig: any + ): StoreEnhancer<{}, ExtraState> { + return createStore => ( + reducer: Reducer, + preloadedState?: any + ) => { + const persistedReducer = persistReducer(persistConfig, reducer) + const store = createStore(persistedReducer, preloadedState) + const persistor = persistStore(store) + + return { + ...store, + replaceReducer: nextReducer => { + return store.replaceReducer( + persistReducer(persistConfig, nextReducer) + ) + }, + persistor + } + } + } + + const store = createStore(reducer, createPersistEnhancer('hi')) + + store.getState().foo + // typings:expect-error + store.getState().wrongField + + const newReducer = ( + state: { test: boolean } = { test: true }, + _: AnyAction + ) => state + + const newStore = store.replaceReducer(newReducer) + newStore.getState().test + // typings:expect-error + newStore.getState().whatever + // typings:expect-error + newStore.getState().wrongField +} diff --git a/test/typescript/injectedDispatch.ts b/test/typescript/injectedDispatch.ts index f115b699fe..46dc9a7016 100644 --- a/test/typescript/injectedDispatch.ts +++ b/test/typescript/injectedDispatch.ts @@ -1,4 +1,4 @@ -import { Dispatch, Action } from 'redux' +import { Dispatch, Action } from '../..' interface Component

{ props: P diff --git a/test/typescript/middleware.ts b/test/typescript/middleware.ts index 7b1c48443c..a0f9afa7f6 100644 --- a/test/typescript/middleware.ts +++ b/test/typescript/middleware.ts @@ -2,13 +2,12 @@ import { Middleware, MiddlewareAPI, applyMiddleware, - StoreEnhancer, createStore, Dispatch, Reducer, Action, AnyAction -} from 'redux' +} from '../..' /** * Logger middleware doesn't add any extra types to dispatch, just logs actions diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index 5a109eeece..f013f83b37 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -1,4 +1,4 @@ -import { Reducer, Action, combineReducers, ReducersMapObject } from 'redux' +import { Reducer, Action, combineReducers, ReducersMapObject } from '../..' /** * Simple reducer definition with no action shape checks. diff --git a/test/typescript/replaceReducer.ts b/test/typescript/replaceReducer.ts new file mode 100644 index 0000000000..03b5b7fd90 --- /dev/null +++ b/test/typescript/replaceReducer.ts @@ -0,0 +1,26 @@ +import { combineReducers, createStore } from '../..' + +/** + * verify that replaceReducer maintains strict typing if the new types change + */ +const bar = (state = { value: 'bar' }) => state +const baz = (state = { value: 'baz' }) => state +const ACTION = { + type: 'action' +} + +const originalCompositeReducer = combineReducers({ bar }) +const store = createStore(originalCompositeReducer) +store.dispatch(ACTION) + +const firstState = store.getState() +firstState.bar.value +// typings:expect-error +firstState.baz.value + +const nextStore = store.replaceReducer(combineReducers({ baz })) // returns -> { baz: { value: 'baz' }} + +const nextState = nextStore.getState() +// typings:expect-error +nextState.bar.value +nextState.baz.value diff --git a/test/typescript/store.ts b/test/typescript/store.ts index 123464ce55..dd7d76296e 100644 --- a/test/typescript/store.ts +++ b/test/typescript/store.ts @@ -4,11 +4,10 @@ import { Reducer, Action, StoreEnhancer, - StoreCreator, - StoreEnhancerStoreCreator, Unsubscribe, - Observer -} from 'redux' + Observer, + ExtendState +} from '../..' import 'symbol-observable' type State = { @@ -19,6 +18,41 @@ type State = { } } +/* extended state */ +const noExtend: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + } +} +// typings:expect-error +const noExtendError: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + }, + e: 'oops' +} + +const yesExtend: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + }, + yes: 'we can' +} +// typings:expect-error +const yesExtendError: ExtendState = { + a: 'a', + b: { + c: 'c', + d: 'd' + } +} + interface DerivedAction extends Action { type: 'a' b: 'b' @@ -56,6 +90,9 @@ const funcWithStore = (store: Store) => {} const store: Store = createStore(reducer) +// ensure that an array-based state works +const arrayReducer = (state: any[] = []) => state || [] +const storeWithArrayState: Store = createStore(arrayReducer) const storeWithPreloadedState: Store = createStore(reducer, { a: 'a', b: { c: 'c', d: 'd' } diff --git a/test/typescript/tsconfig.json b/test/typescript/tsconfig.json index be426fdcac..9af8c36a48 100644 --- a/test/typescript/tsconfig.json +++ b/test/typescript/tsconfig.json @@ -1,10 +1,7 @@ { "compilerOptions": { - "lib": ["es2015", "dom"], + "lib": ["es2017", "dom"], "strict": true, - "baseUrl": "../..", - "paths": { - "redux": ["index.d.ts"] - } + "baseUrl": "../.." } } diff --git a/test/utils/isPlainObject.spec.js b/test/utils/isPlainObject.spec.ts similarity index 93% rename from test/utils/isPlainObject.spec.js rename to test/utils/isPlainObject.spec.ts index ddb7775b46..492ed6d38d 100644 --- a/test/utils/isPlainObject.spec.js +++ b/test/utils/isPlainObject.spec.ts @@ -16,7 +16,7 @@ describe('isPlainObject', () => { expect(isPlainObject(new Date())).toBe(false) expect(isPlainObject([1, 2, 3])).toBe(false) expect(isPlainObject(null)).toBe(false) - expect(isPlainObject()).toBe(false) + expect(isPlainObject(undefined)).toBe(false) expect(isPlainObject({ x: 1, y: 2 })).toBe(true) }) }) diff --git a/test/utils/warning.spec.js b/test/utils/warning.spec.ts similarity index 100% rename from test/utils/warning.spec.js rename to test/utils/warning.spec.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..1ef2e9f083 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,69 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "lib": [ + "dom", + "es2017" + ] /* Specify library files to be included in the compilation. */, + // "allowJs": true /* Allow javascript files to be compiled. */, + // "checkJs": true, /* Report errors in .js files. */ + "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "declaration": true /* Generates corresponding '.d.ts' file. */, + // "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, + "declarationDir": "./types" /* Output directory for generated declaration files. */, + // "emitDeclarationOnly": true /* Only emit ‘.d.ts’ declaration files. */, + "sourceMap": true /* Generates corresponding '.map' file. */, + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./types" /* Redirect output structure to the directory. */, + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + "removeComments": false /* Do not emit comments to output. */, + // "noEmit": true /* Do not emit outputs. */, + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */, + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": true /* Report errors on unused locals. */, + "noUnusedParameters": true /* Report errors on unused parameters. */, + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, + // "paths": { + // "*": ["*", "types/*"] + // } /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "skipLibCheck": true /* This trusts that typings files (@types) are accurate and don't need to be checked by the compiler. This speeds build times. */ + }, + "include": ["./src/**/*"] +}