├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json ├── src ├── class-connect.ts ├── common.ts ├── devtools.ts ├── examples │ ├── class-component.tsx │ └── counter.tsx ├── hook.ts ├── index.ts ├── root.ts └── store │ ├── index.ts │ ├── subscribers.ts │ └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | src 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Gregory Petrosyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wire 2 | 3 | Wire is a state management library for React that emphasizes type safety and fast iteration. 4 | 5 | ## Features 6 | 7 | - No boilerplate 8 | - Async thunks included 9 | - Selectors 10 | - Only one way of doing things which is type-safe and easy to understand 11 | - Mutations are type errors 12 | - React hooks 13 | 14 | 15 | ## Examples 16 | 17 | ### Basic Store with Actions 18 | 19 | ```ts 20 | import { store } from "@wire-ts/wire"; 21 | 22 | interface Todo { 23 | id: number; 24 | task: string; 25 | done: boolean; 26 | } 27 | 28 | export default store({ list: [] as Todo[] }).actions({ 29 | add: (state, todo: Todo) => ({ 30 | list: [...state.list, todo], 31 | }), 32 | remove: (state, id: number) => ({ 33 | list: state.list.filter(todo => todo.id !== id), 34 | }), 35 | }); 36 | ``` 37 | 38 | ### Full-Featured Store with Actions, Thunks, Selectors 39 | 40 | ```ts 41 | import { store } from "@wire-ts/wire"; 42 | 43 | interface Todo { 44 | id: number; 45 | task: string; 46 | done: boolean; 47 | } 48 | 49 | const todos = store({ list: [] as Todo[] }).actions({ 50 | load: (state, todos: Todo[]) => ({ ...state, list: todos }), 51 | add: (state, todo: Todo) => ({ 52 | list: [...state.list, todo], 53 | }), 54 | remove: (state, id: number) => ({ 55 | list: state.list.filter(todo => todo.id !== id), 56 | }), 57 | }).thunks({ 58 | load: async () => { 59 | const items = await yourAPICall(); 60 | todos.actions.load(items); 61 | }, 62 | remove123: (someArg: boolean) => { 63 | console.log(todos.state); 64 | todos.actions.remove(123); 65 | } 66 | }).selectors({ 67 | incomplete: (state) => state.list.filter(t => !t.done) 68 | }); 69 | ``` 70 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wire", 3 | "version": "0.10.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "0.10.1", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "@types/react": "*" 12 | }, 13 | "peerDependencies": { 14 | "react": "*", 15 | "typescript": "^4.1.3" 16 | } 17 | }, 18 | "node_modules/@types/prop-types": { 19 | "version": "15.7.3", 20 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", 21 | "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" 22 | }, 23 | "node_modules/@types/react": { 24 | "version": "17.0.0", 25 | "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", 26 | "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", 27 | "dependencies": { 28 | "@types/prop-types": "*", 29 | "csstype": "^3.0.2" 30 | } 31 | }, 32 | "node_modules/csstype": { 33 | "version": "3.0.5", 34 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", 35 | "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" 36 | }, 37 | "node_modules/js-tokens": { 38 | "version": "4.0.0", 39 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 40 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 41 | "peer": true 42 | }, 43 | "node_modules/loose-envify": { 44 | "version": "1.4.0", 45 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 46 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 47 | "peer": true, 48 | "dependencies": { 49 | "js-tokens": "^3.0.0 || ^4.0.0" 50 | }, 51 | "bin": { 52 | "loose-envify": "cli.js" 53 | } 54 | }, 55 | "node_modules/object-assign": { 56 | "version": "4.1.1", 57 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 58 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 59 | "peer": true, 60 | "engines": { 61 | "node": ">=0.10.0" 62 | } 63 | }, 64 | "node_modules/react": { 65 | "version": "17.0.1", 66 | "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", 67 | "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", 68 | "peer": true, 69 | "dependencies": { 70 | "loose-envify": "^1.1.0", 71 | "object-assign": "^4.1.1" 72 | }, 73 | "engines": { 74 | "node": ">=0.10.0" 75 | } 76 | }, 77 | "node_modules/typescript": { 78 | "version": "4.1.3", 79 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 80 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", 81 | "peer": true, 82 | "bin": { 83 | "tsc": "bin/tsc", 84 | "tsserver": "bin/tsserver" 85 | }, 86 | "engines": { 87 | "node": ">=4.2.0" 88 | } 89 | } 90 | }, 91 | "dependencies": { 92 | "@types/prop-types": { 93 | "version": "15.7.3", 94 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", 95 | "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" 96 | }, 97 | "@types/react": { 98 | "version": "17.0.0", 99 | "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.0.tgz", 100 | "integrity": "sha512-aj/L7RIMsRlWML3YB6KZiXB3fV2t41+5RBGYF8z+tAKU43Px8C3cYUZsDvf1/+Bm4FK21QWBrDutu8ZJ/70qOw==", 101 | "requires": { 102 | "@types/prop-types": "*", 103 | "csstype": "^3.0.2" 104 | } 105 | }, 106 | "csstype": { 107 | "version": "3.0.5", 108 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz", 109 | "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ==" 110 | }, 111 | "js-tokens": { 112 | "version": "4.0.0", 113 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 114 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 115 | "peer": true 116 | }, 117 | "loose-envify": { 118 | "version": "1.4.0", 119 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 120 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 121 | "peer": true, 122 | "requires": { 123 | "js-tokens": "^3.0.0 || ^4.0.0" 124 | } 125 | }, 126 | "object-assign": { 127 | "version": "4.1.1", 128 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 129 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 130 | "peer": true 131 | }, 132 | "react": { 133 | "version": "17.0.1", 134 | "resolved": "https://registry.npmjs.org/react/-/react-17.0.1.tgz", 135 | "integrity": "sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==", 136 | "peer": true, 137 | "requires": { 138 | "loose-envify": "^1.1.0", 139 | "object-assign": "^4.1.1" 140 | } 141 | }, 142 | "typescript": { 143 | "version": "4.1.3", 144 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", 145 | "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", 146 | "peer": true 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wire-ts/wire", 3 | "version": "0.10.1", 4 | "main": "dist/index.js", 5 | "license": "MIT", 6 | "author": "Gregory ", 7 | "homepage": "https://wire-ts.github.io", 8 | "description": "A type-safe data store for React", 9 | "keywords": [ 10 | "react", 11 | "state", 12 | "store", 13 | "typescript", 14 | "connect", 15 | "redux" 16 | ], 17 | "scripts": { 18 | "build": "rm -rf dist && tsc", 19 | "test": "tsc --noEmit" 20 | }, 21 | "peerDependencies": { 22 | "react": "*", 23 | "typescript": "^4.1.3" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "*" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/wire-ts/wire.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/wire-ts/wire/issues" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/class-connect.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Store, keys } from "./common"; 3 | 4 | export interface ClassConnector { 5 | (Component: React.ComponentType): React.ComponentType; 6 | } 7 | 8 | export type Props = T extends ClassConnector ? P & O : never; 9 | 10 | const makeClassConnector = >>(map: T) => < 11 | MP, 12 | CP 13 | >( 14 | getProps: (props: T, ownProps: CP) => MP 15 | ): ClassConnector => (Component: React.ComponentType) => 16 | class extends React.PureComponent { 17 | static displayName = `Wired${Component.name}`; 18 | mounted = false; 19 | unsubs: Array<() => void> = []; 20 | 21 | rerender = () => { 22 | if (this.mounted) { 23 | this.forceUpdate(); 24 | } 25 | }; 26 | 27 | componentDidMount() { 28 | this.mounted = true; 29 | this.unsubs = keys(map).map((k) => map[k].subscribe(this.rerender)); 30 | } 31 | 32 | componentWillUnmount() { 33 | this.mounted = false; 34 | this.unsubs.forEach((unsub) => unsub()); 35 | this.unsubs = []; 36 | } 37 | 38 | render() { 39 | return React.createElement(Component, { 40 | ...this.props, 41 | ...getProps(map, this.props), 42 | }); 43 | } 44 | }; 45 | 46 | export default makeClassConnector; 47 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | export type SubscribeFn = ((method: string) => void) | (() => void); 2 | 3 | export interface Store { 4 | state: S; 5 | subscribe: (f: SubscribeFn) => () => void; 6 | } 7 | 8 | export type AllExceptAny = undefined 9 | | null 10 | | boolean 11 | | string 12 | | number 13 | | Function 14 | | Map 15 | | Set 16 | | object; 17 | 18 | type ImmutablePrimitive = 19 | | undefined 20 | | null 21 | | boolean 22 | | string 23 | | number 24 | | Function; 25 | export type ImmutableMap = ReadonlyMap, Immutable>; 26 | export type ImmutableSet = ReadonlySet>; 27 | export type ImmutableObject = { readonly [K in keyof T]: Immutable }; 28 | 29 | export type Immutable = T extends ImmutablePrimitive 30 | ? T 31 | : T extends Map 32 | ? ImmutableMap 33 | : T extends Set 34 | ? ImmutableSet 35 | : ImmutableObject; 36 | 37 | export const deepCopy = | object>(object: T): T => { 38 | let outObject: any, value, key; 39 | 40 | if (typeof object !== "object" || object === null) { 41 | return object; // Return the value if inObject is not an object 42 | } 43 | 44 | // Create an array or object to hold the values 45 | outObject = Array.isArray(object) ? [] : {}; 46 | 47 | for (key in object) { 48 | value = (object)[key]; 49 | 50 | // Recursively (deep) copy for nested objects, including arrays 51 | outObject[key] = deepCopy(value); 52 | } 53 | 54 | return outObject; 55 | }; 56 | 57 | export const keys = (object: T) => 58 | Object.keys(object) as Array; 59 | -------------------------------------------------------------------------------- /src/devtools.ts: -------------------------------------------------------------------------------- 1 | import { Store, deepCopy, keys } from "./common"; 2 | 3 | const getStateTree = >>(root: T) => 4 | keys(root).reduce( 5 | (acc, key) => ({ ...acc, [key]: deepCopy(root[key].state) }), 6 | {} as { [k in keyof T]: T[k]["state"] } 7 | ); 8 | 9 | export const enable = >>(map: T) => { 10 | const stateMap = getStateTree(map); 11 | let extensionReady = false; 12 | let outstandingMsgs: object[] = []; 13 | 14 | const postMessage = (msg: object) => { 15 | if (extensionReady) { 16 | window.postMessage(msg, "*"); 17 | } else { 18 | outstandingMsgs.push(msg); 19 | } 20 | }; 21 | 22 | postMessage({ 23 | type: "WIRE_INIT", 24 | stateMap, 25 | }); 26 | 27 | keys(map).map((k) => 28 | map[k].subscribe((method) => { 29 | postMessage({ 30 | type: "WIRE_CHANGE", 31 | store: k, 32 | method, 33 | oldState: deepCopy(stateMap[k]), 34 | newState: deepCopy(map[k].state), 35 | }); 36 | 37 | stateMap[k] = deepCopy(map[k].state); 38 | }) 39 | ); 40 | 41 | window.addEventListener("message", (e) => { 42 | if (typeof e.data === "object" && e.data.type === "WIRE_EXT_READY") { 43 | extensionReady = true; 44 | outstandingMsgs.forEach(postMessage); 45 | outstandingMsgs = []; 46 | } 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /src/examples/class-component.tsx: -------------------------------------------------------------------------------- 1 | import { root } from "./counter"; 2 | import { Props } from "../class-connect"; 3 | import React from "react"; 4 | 5 | interface InProps { 6 | id: number; 7 | } 8 | 9 | const connect = root.connect((r, _: InProps) => r.todos); 10 | 11 | class App extends React.Component> { 12 | render() { 13 | const { state: { list }, } = this.props; 14 | return
{list.length}
; 15 | } 16 | } 17 | 18 | export const Connected = connect(App); 19 | 20 | export const Test = () => ; 21 | -------------------------------------------------------------------------------- /src/examples/counter.tsx: -------------------------------------------------------------------------------- 1 | import { rootStore, store } from ".."; 2 | 3 | interface Todo { 4 | id: number; 5 | task: string; 6 | done: boolean; 7 | } 8 | 9 | const todos = store({ list: [] as Todo[] }) 10 | .actions({ 11 | add: (state, newItem: Todo) => ({ 12 | ...state, 13 | list: [...state.list, newItem], 14 | }), 15 | }) 16 | .selectors({ 17 | incomplete: (state) => state.list.filter((t) => !t.done), 18 | }); 19 | 20 | export const root = rootStore({ 21 | todos, 22 | }); 23 | -------------------------------------------------------------------------------- /src/hook.ts: -------------------------------------------------------------------------------- 1 | import { Immutable, Store } from "./common"; 2 | import React from "react"; 3 | 4 | const createHook = >>(map: T) => ( 5 | f: (props: T) => MP 6 | ) => { 7 | const [computed, setComputed] = React.useState(f(map)); 8 | const [_, forceUpdate] = React.useReducer((x) => x + 1, 0); 9 | 10 | const updateProps = () => { 11 | setComputed(f(map)); 12 | forceUpdate(); 13 | }; 14 | 15 | React.useEffect(() => { 16 | const unsubs = Object.keys(map).map((k) => 17 | map[k as keyof T].subscribe(updateProps) 18 | ); 19 | 20 | return () => { 21 | unsubs.forEach((unsub) => unsub()); 22 | }; 23 | }, []); 24 | 25 | return computed as Immutable; 26 | }; 27 | 28 | export default createHook; 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as rootStore } from "./root"; 2 | export { default as store } from "./store"; 3 | export { Props } from "./class-connect" 4 | -------------------------------------------------------------------------------- /src/root.ts: -------------------------------------------------------------------------------- 1 | import { Immutable, Store } from "./common"; 2 | import { enable as enableDevtools } from "./devtools"; 3 | import createHook from "./hook"; 4 | import makeClassConnector from "./class-connect"; 5 | 6 | const rootStore = >>(map: T) => ({ 7 | enableDebugging: () => enableDevtools(map), 8 | getState: () => map as Immutable, 9 | // connector for hooks 10 | useStore: createHook(map), 11 | // connector for class-based components 12 | connect: makeClassConnector(map), 13 | }); 14 | 15 | export default rootStore; 16 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { Immutable } from "../common"; 2 | import { Input, Output } from "./types"; 3 | import Subscribers from "./subscribers"; 4 | 5 | const createStore = (initialState: S): Input.StoreWithState => ({ 6 | actions: >(actions: A) => { 7 | const subscribers = new Subscribers(); 8 | 9 | const store: Output.Store = { 10 | state: initialState as Immutable, 11 | subscribe: subscribers.add, 12 | actions: Object.entries(actions).reduce( 13 | (acc, [key, action]) => ({ 14 | ...acc, 15 | [key]: (...args: never[]) => { 16 | store.state = action(store.state, ...args); 17 | subscribers.broadcast(key); 18 | }, 19 | }), 20 | {} as Output.Actions 21 | ), 22 | thunks: (thunks: T) => { 23 | (store as any).thunks = thunks; 24 | return store as Output.Store, null>; 25 | }, 26 | selectors: >(selectors: I) => { 27 | const castStore = store as Output.Store< 28 | S, 29 | A, 30 | null, 31 | Output.Selectors 32 | >; 33 | 34 | for (const [key, selector] of Object.entries(selectors)) { 35 | (castStore.selectors as Output.Selectors)[key as keyof I] = (( 36 | ...args: never[] 37 | ) => selector(store.state, ...args)) as Output.Selectors< 38 | S, 39 | I 40 | >[keyof Output.Selectors]; 41 | } 42 | 43 | return castStore; 44 | }, 45 | }; 46 | 47 | return store; 48 | }, 49 | }); 50 | 51 | export default createStore; 52 | -------------------------------------------------------------------------------- /src/store/subscribers.ts: -------------------------------------------------------------------------------- 1 | import { SubscribeFn } from "../common"; 2 | 3 | export default class Subscribers { 4 | private i = 0; 5 | private subscribers = new Map(); 6 | 7 | add = (f: SubscribeFn) => { 8 | const index = this.i; 9 | this.subscribers.set(index, f); 10 | this.i++; 11 | 12 | return () => this.subscribers.delete(index); 13 | }; 14 | 15 | broadcast = (method: string) => { 16 | for (const callback of this.subscribers.values()) { 17 | callback(method); 18 | } 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/store/types.ts: -------------------------------------------------------------------------------- 1 | import { SubscribeFn, Immutable } from "../common"; 2 | 3 | export namespace Input { 4 | export type Actions = Record< 5 | string, 6 | (state: Immutable, ...args: never[]) => Immutable 7 | >; 8 | 9 | export type Thunks = Record< 10 | string, 11 | (...args: never[]) => void | Promise 12 | >; 13 | 14 | export type StoreWithState = { 15 | actions: >( 16 | actions: A 17 | ) => Output.Store; 18 | }; 19 | 20 | export type Selectors = Record< 21 | string, 22 | (state: Immutable, ...args: never[]) => any 23 | >; 24 | } 25 | 26 | export namespace Output { 27 | export type Actions> = { 28 | [k in keyof A]: A[k] extends ( 29 | s: Immutable, 30 | ...args: infer Args 31 | ) => Immutable 32 | ? (...args: Args) => void 33 | : never; 34 | }; 35 | 36 | export type Selectors> = { 37 | [k in keyof I]: I[k] extends ( 38 | state: Immutable, 39 | ...args: infer Args 40 | ) => infer O 41 | ? (...args: Args) => O 42 | : never; 43 | }; 44 | 45 | export type Thunks = T; 46 | 47 | export type Store, T, Sel> = { 48 | state: Immutable; 49 | subscribe: (f: SubscribeFn) => () => void; 50 | actions: Actions; 51 | thunks: T extends null 52 | ? ( 53 | thunks: I 54 | ) => Store, Sel> 55 | : T; 56 | selectors: Sel extends null 57 | ? >( 58 | selectors: I 59 | ) => Store> 60 | : Sel; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2015", 5 | "lib": ["ES2015", "ES2019", "DOM"], 6 | "module": "commonjs", 7 | "jsx": "react", 8 | "strict": true, 9 | "strictFunctionTypes": true, 10 | "strictBindCallApply": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "baseUrl": "src", 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "declaration": true, 19 | "outDir": "dist" 20 | } 21 | } 22 | --------------------------------------------------------------------------------