├── .gitignore ├── .gitlab-ci.yml ├── .prettierrc ├── LICENSE ├── README.md ├── examples ├── simple-todo │ ├── README.md │ ├── src │ │ ├── index.html │ │ └── todos.tsx │ └── webpack.config.js └── todomvc │ ├── .gitignore │ ├── README.md │ ├── src │ ├── components │ │ ├── Todo.tsx │ │ └── TodoList.tsx │ ├── index.html │ ├── index.tsx │ └── reducers │ │ ├── Filter.ts │ │ └── Todo.ts │ └── webpack.config.js ├── package.json ├── src ├── action-creators │ └── index.ts ├── connect │ └── index.ts ├── constants.ts ├── handler-map │ ├── index.test.ts │ └── index.ts ├── index.ts ├── reducer │ ├── index.test.ts │ └── index.ts ├── store │ ├── index.test.ts │ └── index.ts └── types │ ├── converters.ts │ ├── helpers.ts │ └── redux.ts ├── test └── types │ ├── ts-tests.ts │ └── type-helpers.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock 4 | dist 5 | coverage 6 | .vscode -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:4 2 | 3 | before_script: 4 | - npm install 5 | 6 | test_job: 7 | stage: test 8 | script: 9 | - npm test -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "printWidth": 80, 4 | "tabWidth": 4, 5 | "singleQuote": true 6 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 Paul Körbitz 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 | # typeful-redux 2 | 3 | [![npm version](https://img.shields.io/npm/v/typeful-redux.svg?style=flat-square)](https://www.npmjs.com/package/typeful-redux) 4 | 5 | A type-safe, low boilerplate wrapper for redux to be used in TypeScript projects. 6 | 7 | ## Elevator pitch 8 | 9 | This is how you create a reducer and a store with typeful-redux. Note that all 10 | calls are fully type-safe and will trigger type errors when used incorrectly. 11 | 12 | ```TypeScript 13 | interface Todo { 14 | task: string; 15 | completed: boolean; 16 | } 17 | 18 | // This map of handlers contains all the information we need 19 | // to create fully type-safe reducers and (bound or unbound) 20 | // action creators 21 | const todoHandler = { 22 | CLEAR: (_state: Todo[]) => [] as Todo[], 23 | ADD: (state: Todo[], newTodo: Todo) => [...state, newItem], 24 | TOGGLE: (state: Todo[], index: number) => [ 25 | ...state.slice(0, index), 26 | { task: state[index].task, completed: !state[index].completed }, 27 | ...state.slice(index + 1) 28 | ] 29 | }; 30 | 31 | const initialState: Todo[] = []; 32 | 33 | // todoReducer has the type information on what state and 34 | // actions it can reduce 35 | const todoReducer = createReducer(initialState, todoHandler); 36 | 37 | // Creates fully typed action creators for CLEAR, ADD, TOGGLE 38 | const actionCreators = createActionCreators(todoHandler); 39 | 40 | // Create the store - combineReducers is not needed but works ;) 41 | const store = createStore( 42 | combineReducers({ todos: todoReducer }) 43 | ); 44 | 45 | // The state has type: { todos: Todo[] } 46 | const state = store.getState(); 47 | 48 | // type error: action has the wrong form - expected just { type: 'CLEAR' } 49 | store.dispatch({ type: 'CLEAR', payload: 'unexpected payload' }); 50 | // type error: missing payload 51 | store.dispatch({ type: 'ADD' }); 52 | 53 | // These all typecheck 54 | store.dispatch(actionCreators.ADD({ task: 'new todo', completed: false })); 55 | store.dispatch(actionCreators.TOGGLE(0)); 56 | store.dispatch(actionCreators.CLEAR()); 57 | 58 | // Bound action creators dispatch directly - convenient for mapDispatchToProps 59 | // This is equivalent to the above 60 | const boundCreators = bindActionCreators(actionCreators, store.dispatch); 61 | boundCreators.ADD({ task: 'new todo', completed: false }); 62 | boundCreators.TOGGLE(0); 63 | boundCreators.CLEAR(); 64 | ``` 65 | 66 | A very simple, runnable example app can be found [here](./examples/simple-todo/). A TodoMVC implementation with 67 | slightly more features is availabe [here](./examples/todomvc/). 68 | 69 | ## Motivation 70 | 71 | [redux] is a fantastic approach to manage state in single page applications. 72 | Unfortunately, vanilla redux requires some boilerplate and is hard to use 73 | in a type-safe way. 74 | 75 | [typeful-redux]'s goal is to make it easy to use redux in a fully type-safe way while also reducing the amount of boilerplate required. This means the redux `getState` and `dispatch` functions need to have the right types and these types should be maintained when using the [react-redux] `connect` function. Furthermore, typeful-redux also provides helper functions 76 | to easily create fully type-safe bound and un-bound action creators. 77 | 78 | More specifically, [typeful-redux] seeks to address the following challenges when using redux: 79 | 80 | - **Full type safety:** redux makes it hard to fully type the `dispatch` 81 | method, to guarantee that only actions are dispatched which are handled by the store or that the dispatched actions are type correct (i.e. have the right payload). 82 | 83 | typeful-redux creates a store that gives a fully type-safe dispatch object, where every action is available as a function expecting the right payload. The `getState` method is also fully typed and returns a state with the right type. 84 | 85 | - **Low Boilerplate:** redux needs actions, possibly action creators and reducers. 86 | When trying to set this up in a type-safe way, many things need to be written down twice (or more). This introduces an opportunity for inconsistencies and errors. 87 | 88 | In typeful-redux, actions and their reducers are defined simultaneously,reducing the amount of code that needs to be written and maintained. 89 | 90 | - **Avoid inconsistencies:** When actions and reducers are defined 91 | seperately, there is the potential to forget handeling an action (or to misspell a type in a reducer's switch statement). typeful-redux makes this impossible by requiring the simultaneous definition of an action with its reducing code. 92 | 93 | Besides these differences and different surface appearence, typeful-redux **is not an alternative redux implementation**, it is just a thin wrapper around reducer and store creation. 94 | In fact the createStore and combineReducer functions are exactly the functions from redux, 95 | they are just typed differently. All the existing redux ecosystem should be usable with this library. Please file an issue if you have trouble using a redux library with typeful-redux. 96 | 97 | ## Documentation 98 | 99 | typeful-redux exports a few functions and type operators to make type-safe store and 100 | action creator creations a breeze. All functions and operators are described here. Also see the [examples](./examples/) for example usages. If you find the documentation insufficient please file an issue or complain to me via email (see profile). 101 | 102 | ### typeful-redux functions and concepts 103 | 104 | #### HandlerMap 105 | 106 | A key concept in typeful-redux is the `HandlerMap`, an object 107 | from action names to handler functions which is used to create 108 | the reducer and action creators. The idea is that this object 109 | contains all the naming and type information and thus it is 110 | not necessary to type any more than that (pun intended!). 111 | 112 | #### `Reducer` 113 | 114 | A simple type capturing the type of reducers: 115 | 116 | ```TypeScript 117 | type Reducer = (state: State, action: Actions) => State; 118 | ``` 119 | 120 | #### `createReducer` 121 | 122 | Takes an initial state and a *HandlerMap* and creates a reducer with 123 | correctly typed state and action arguments. 124 | 125 | It's full type signature is 126 | 127 | ```TypeScript 128 | createReducer: any; }>( 129 | handler: HandlerMap 130 | ): Reducer, ActionsFromHandlerMap>; 131 | ``` 132 | 133 | `StateFromHandlerMap` and `ActionsFromHandlerMap` are type operators 134 | which extract the `State` and `Actions` types from the handler map. 135 | 136 | #### `combineReducers` 137 | 138 | This is the original combineReducers function from redux, the type 139 | signature has been augmented to merge the state and action types so 140 | that the resulting reducer is again fully typed. 141 | 142 | #### `createStore` 143 | 144 | This is the original `createStore` from redux, the type signature 145 | has been augmented to fully capture the action types to give a 146 | fully type-safe dispatch function. 147 | 148 | #### `createActionCreators` 149 | 150 | Takes a handler map and returns an object with type-safe action 151 | creators. The action creators have the same name as the action 152 | and either accept no arguments (for actions without payload) or 153 | a single object which has the same type as the second argument 154 | of the handler function. 155 | 156 | #### `bindActionCreators` 157 | 158 | Takes an object of action creators and a store's dispatch 159 | method and returns an object of bound creators, meaning these 160 | functions directly dispatch the actions. 161 | 162 | #### `connect` 163 | 164 | This is the original react-redux `connect` function with the type 165 | augmented so that the type requirements from the connected component 166 | correctly propagate through `mapStateToProps` and `mapDispatchToProps`. 167 | This way, the connected component requires a store with the correct 168 | `getState` and `dispatch` methods. 169 | 170 | The type of `connect` prioritizes type corectness over convenience and 171 | currently only supports invokation with both 172 | `mapStateToProps` and `mapDispatchToProps`. However, it is easy enough 173 | to just supply the identity function with either if the state or 174 | dispatch does not need to be modified. The upside is that non-alignment 175 | between state and dispatch and a connected components needs will be 176 | caught as a type error. 177 | 178 | ### Usage with React's Context API 179 | 180 | React 16.3 provides a [new context API](https://reactjs.org/docs/context.html#reactcreatecontext) which is actually possible to use 181 | type correctly. So we're fans ;) 182 | 183 | This is how you can use it with typeful-redux: 184 | 185 | ```TypeScript 186 | const store = createStore(/* ... */); 187 | const { Provider: StoreProvider, Consumer: StoreConsumer } = 188 | React.createContext(store); 189 | 190 | render( 191 | 192 | /* ... */ 193 | 194 | {store => } 195 | 196 | /* ... */ 197 | , 198 | document.getElementById("app") 199 | ); 200 | ``` 201 | 202 | ### Usage with redux-thunk 203 | 204 | With the new API since version 0.4 it should be easier to use typeful-redux 205 | with redux-thunk. An example is still in the works and some types might need 206 | a little tweaking, but there should be something new here soon! 207 | 208 | ### Usage with redux-saga 209 | 210 | Just as with redux-thunk, usage together with redux-saga is not fully thought through 211 | yet, however, we're able to extract all action information from a redux store, 212 | so typeful-redux *should* be able to provide a great basis for a fully-typed 213 | redux-saga experience. Stay tuned. 214 | 215 | ## License 216 | 217 | MIT 218 | 219 | [redux]: http://redux.js.org 220 | [react-redux]: https://github.com/reactjs/react-redux 221 | [typeful-redux]: https://gitlab.com/paul.koerbitz/typeful-redux 222 | -------------------------------------------------------------------------------- /examples/simple-todo/README.md: -------------------------------------------------------------------------------- 1 | # Simple Todo App Example 2 | 3 | This is a simple example of a todo app using typeful-redux. The todo app has a minimal feature set to keep it as short as possible. All code is in one file to simplify the overview. The file comes in at 72 lines of code. 4 | 5 | ## Setup 6 | 7 | Use `npm` or `yarn` to install: 8 | 9 | `$ npm install` 10 | 11 | ## Run 12 | 13 | `$ npm start` runs the app via webpack-dev-server. Visit `localhost:8080`. 14 | -------------------------------------------------------------------------------- /examples/simple-todo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/simple-todo/src/todos.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { 4 | createReducer, 5 | createStore, 6 | createActionCreators, 7 | bindActionCreators, 8 | connect, 9 | createHandlerMap 10 | } from '../../../src'; 11 | 12 | interface TodoItem { 13 | task: string; 14 | completed: boolean; 15 | } 16 | 17 | const TodoHandlers = createHandlerMap([] as TodoItem[], { 18 | ADD: (s, task: string) => [...s, { task, completed: false }], 19 | CLEAR: [], 20 | TOGGLE: (s, idx: number) => [ 21 | ...s.slice(0, idx), 22 | { task: s[idx].task, completed: !s[idx].completed }, 23 | ...s.slice(idx + 1) 24 | ] 25 | }); 26 | 27 | const TodoReducer = createReducer(TodoHandlers); 28 | 29 | const store = createStore(TodoReducer); 30 | 31 | const actionCreators = createActionCreators(TodoHandlers); 32 | const boundCreators = bindActionCreators(actionCreators, store.dispatch); 33 | 34 | type Dispatch = typeof store.dispatch; 35 | 36 | 37 | interface TodoProps { 38 | item: TodoItem; 39 | toggle(): void; 40 | } 41 | 42 | const TodoComponent = (p: TodoProps) => ( 43 |
  • 44 | 45 | {p.item.task} 46 |
  • 47 | ); 48 | 49 | type TodoListProps = { 50 | todos: TodoItem[]; 51 | } & typeof boundCreators; 52 | 53 | class TodoListComponent extends React.Component { 54 | private input: HTMLInputElement | null = null; 55 | 56 | private handleSubmit = (e: React.FormEvent) => { 57 | e.preventDefault(); 58 | if (this.input != null) { 59 | this.props.ADD(this.input.value); 60 | this.input.value = ''; 61 | } 62 | }; 63 | 64 | render() { 65 | const { todos, CLEAR, TOGGLE } = this.props; 66 | const items = todos.map((todo, idx) => ( 67 | TOGGLE(idx)} /> 68 | )); 69 | 70 | return ( 71 |
    72 |
    73 | (this.input = input)} /> 74 | 75 |
    76 |
      {items}
    77 |
    78 | 79 |
    80 |
    81 | ); 82 | } 83 | } 84 | 85 | const mapStateToProps = (state: TodoItem[]) => ({ todos: state }); 86 | 87 | const mapDisptachToProps = (dispatch: Dispatch) => bindActionCreators(actionCreators, dispatch); 88 | 89 | const TodoListContainer = connect(mapStateToProps, mapDisptachToProps)( 90 | TodoListComponent 91 | ); 92 | 93 | render(, document.getElementById('app')); 94 | -------------------------------------------------------------------------------- /examples/simple-todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/todos.tsx', 7 | devtool: 'source-map', 8 | plugins: [new HtmlWebpackPlugin({ 9 | title: "Typeful Todos", 10 | template: "./src/index.html", 11 | hash: true, 12 | inject: true 13 | })], 14 | output: { 15 | path: path.join(__dirname, '/dist'), 16 | filename: "[name].js", 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'], 20 | modules: ['node_modules'] 21 | }, 22 | module: { 23 | rules: [{ 24 | test: /\.tsx?$/, 25 | loader: 'ts-loader', 26 | exclude: ['node_modules'] 27 | }, { 28 | test: /\.css$/, 29 | loader: ['style-loader', 'css-loader'] 30 | }] 31 | }, 32 | devServer: { 33 | historyApiFallback: true, 34 | disableHostCheck: true, 35 | watchContentBase: false, 36 | watchOptions: { 37 | watch: true 38 | } 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /examples/todomvc/README.md: -------------------------------------------------------------------------------- 1 | # TodoMVC Example 2 | 3 | This example shows an implementation of the [TodoMVC](http://todomvc.com/) app. 4 | It implements most of the functionality of the TodoMVC app. Routing as well as 5 | saving to and loading from local storage are not yet implemented. 6 | 7 | ## Setup 8 | 9 | Use `npm` or `yarn` to install. Example: 10 | 11 | `$ npm install` 12 | 13 | ## Run 14 | 15 | `$ npm start` runs the app via webpack-dev-server. Visit `localhost:8080`. 16 | 17 | ## Overview 18 | 19 | There are two reducers, `TodoReducer` and `FilterReducer` which live in the 20 | [src/reducers/](./src/reducers/) directory. They are quite small and come in at 23 and 7 lines of code, respectively. 21 | 22 | The two components `TodoComponent` and `TodoListComponent` are placed in the 23 | [src/components/](./src/components/) directory. 24 | 25 | The main file is [./src/index.tsx](./src/index.tsx), it uses the reducers to 26 | create a store and creates the main application by connecting the store 27 | to the TodoListComponent. -------------------------------------------------------------------------------- /examples/todomvc/src/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { TodoItem } from '../reducers/Todo'; 3 | 4 | export interface TodoProps { 5 | item: TodoItem; 6 | toggle(): void; 7 | edit(task: string): void; 8 | delete(): void; 9 | } 10 | 11 | export interface TodoState { 12 | editedTask: string | undefined; 13 | } 14 | 15 | const ENTER_KEY_CODE = 13; 16 | 17 | export class TodoComponent extends React.Component { 18 | constructor(p: TodoProps) { 19 | super(p); 20 | this.state = { editedTask: undefined }; 21 | } 22 | private edit = () => { 23 | this.setState({ editedTask: this.props.item.task }); 24 | } 25 | private handleChange = (e: React.ChangeEvent) => { 26 | const editedTask = e.target.value; 27 | this.setState({ editedTask }); 28 | } 29 | private handleSubmit = (e: React.KeyboardEvent) => { 30 | if (e.keyCode === ENTER_KEY_CODE && this.state.editedTask != undefined) { 31 | this.props.edit(this.state.editedTask.trim()); 32 | this.setState({ editedTask: undefined }); 33 | } 34 | } 35 | render() { 36 | const { toggle, item } = this.props; 37 | const liClass = 38 | (item.completed ? "completed" : "") + 39 | (this.state.editedTask != undefined ? " editing" : ""); 40 | 41 | const savingIndicatorOpacity = item.isSaved ? 0 : 0.4; 42 | 43 | return ( 44 |
  • 45 |
    46 | 52 | 53 | Saving... 54 |
    56 | this.setState({ editedTask: undefined })} 63 | /> 64 |
  • 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/todomvc/src/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import * as Todo from '../reducers/Todo'; 4 | import * as Filter from '../reducers/Filter'; 5 | 6 | import { TodoComponent } from './Todo'; 7 | 8 | export type TodoListProps = { 9 | todos: Todo.TodoItem[]; 10 | filter: Filter.FilterType; 11 | } & Todo.Dispatch & 12 | Filter.Dispatch; 13 | 14 | export class TodoListComponent extends React.Component { 15 | private input: HTMLInputElement | null = null; 16 | 17 | private handleSubmit = async (e: React.FormEvent) => { 18 | e.preventDefault(); 19 | if (this.input != null && this.input.value !== '') { 20 | const value = this.input.value; 21 | this.input.value = ''; 22 | await this.props.ADD_TODO(value); 23 | } 24 | }; 25 | 26 | render() { 27 | const { 28 | todos, 29 | CLEAR_TODOS, 30 | TOGGLE_TODO, 31 | EDIT_TODO, 32 | REMOVE_TODO, 33 | CLEAR_COMPLETED, 34 | filter, 35 | FILTER_ALL, 36 | FILTER_ACTIVE, 37 | FILTER_COMPLETED, 38 | } = this.props; 39 | 40 | const visible = 41 | filter === 'all' 42 | ? todos 43 | : filter === 'active' 44 | ? todos.filter(todo => !todo.completed) 45 | : todos.filter(todo => todo.completed); 46 | 47 | const itemName = visible.length === 1 ? 'item' : 'items'; 48 | 49 | const items = visible.map((todo, idx) => ( 50 | TOGGLE_TODO(idx)} 54 | edit={task => EDIT_TODO({ idx, task })} 55 | delete={() => REMOVE_TODO(idx)} 56 | /> 57 | )); 58 | 59 | const clearCompletedButton = todos.some(todo => todo.completed) ? ( 60 | 63 | ) : ( 64 | undefined 65 | ); 66 | 67 | return ( 68 |
    69 |
    70 |

    todos

    71 |
    72 | (this.input = input)} 77 | /> 78 |
    79 |
    80 |
    81 |
      {items}
    82 |
    83 |
    84 | 85 | {todos.length} {itemName} left 86 | 87 |
      88 |
    • 89 | 92 |
    • 93 |
    • 94 | 100 |
    • 101 |
    • 102 | 108 |
    • 109 |
    110 | {clearCompletedButton} 111 |
    112 |
    113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /examples/todomvc/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/todomvc/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'todomvc-app-css/index.css'; 2 | import * as React from 'react'; 3 | import { render } from 'react-dom'; 4 | import { 5 | applyMiddleware, 6 | combineReducers, 7 | bindActionCreators, 8 | connect, 9 | createStore, 10 | StoreState, 11 | StateFromReducer, 12 | ActionsFromReducer, 13 | thunk, 14 | ThunkMiddleware 15 | } from '../../../src'; 16 | import * as Todo from './reducers/Todo'; 17 | import * as Filter from './reducers/Filter'; 18 | import { TodoListComponent } from './components/TodoList'; 19 | 20 | const reducer = combineReducers({ 21 | todos: Todo.reducer, 22 | filter: Filter.reducer 23 | }); 24 | 25 | type RState = StateFromReducer; 26 | type RAction = ActionsFromReducer; 27 | 28 | const middleware = applyMiddleware( 29 | thunk as ThunkMiddleware 30 | ); 31 | 32 | const store = middleware(createStore)(reducer); 33 | 34 | type Store = typeof store; 35 | 36 | export type State = StoreState; 37 | export type Dispatch = typeof store.dispatch; 38 | 39 | const mapStateToProps = (state: State) => state; 40 | 41 | const mapDisptachToProps = (dispatch: Dispatch) => ({ 42 | ...bindActionCreators(Todo.actionCreators, dispatch), 43 | ...bindActionCreators(Filter.actionCreators, dispatch) 44 | }); 45 | 46 | const TodoListContainer = connect( 47 | mapStateToProps, 48 | mapDisptachToProps 49 | )(TodoListComponent); 50 | 51 | render(, document.getElementById('app')); 52 | -------------------------------------------------------------------------------- /examples/todomvc/src/reducers/Filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createHandlerMap, 3 | createReducer, 4 | createActionCreators, 5 | BoundCreatorsFromActionCreators 6 | } from '../../../../src'; 7 | 8 | export type FilterType = 'all' | 'active' | 'completed'; 9 | 10 | const initialState = 'all' as FilterType; 11 | 12 | const handler = createHandlerMap(initialState, { 13 | FILTER_ALL: 'all', 14 | FILTER_ACTIVE: 'active', 15 | FILTER_COMPLETED: 'completed' 16 | }); 17 | 18 | export const reducer = createReducer(handler); 19 | export const actionCreators = createActionCreators(handler); 20 | export type Dispatch = BoundCreatorsFromActionCreators; 21 | -------------------------------------------------------------------------------- /examples/todomvc/src/reducers/Todo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createReducer, 3 | createActionCreators, 4 | BoundCreatorsFromActionCreators, 5 | createHandlerMap 6 | } from '../../../../src'; 7 | import { State, Dispatch } from '..'; 8 | 9 | export interface TodoItem { 10 | task: string; 11 | completed: boolean; 12 | isSaved: boolean; 13 | } 14 | 15 | const replace = (array: ReadonlyArray, idx: number, newValue?: T): T[] => 16 | (newValue == undefined) 17 | ? [...array.slice(0, idx), ...array.slice(idx + 1)] 18 | : [...array.slice(0, idx), newValue, ...array.slice(idx + 1)]; 19 | 20 | const handler = createHandlerMap([] as TodoItem[], { 21 | CREATE_TODO: (s, task: string) => [...s, { task, completed: false, isSaved: false }], 22 | MARK_TODO_SAVED: (s, idx: number) => replace(s, idx, { ...s[idx], isSaved: true }), 23 | CLEAR_TODOS: [], 24 | CLEAR_COMPLETED: (s: TodoItem[]) => s.filter(item => !item.completed), 25 | TOGGLE_TODO: (s, idx: number) => replace(s, idx, { ...s[idx], completed: !s[idx].completed }), 26 | EDIT_TODO: (s, p: { idx: number; task: string }) => 27 | replace(s, p.idx, { ...s[p.idx], task: p.task }), 28 | REMOVE_TODO: (s, idx: number) => replace(s, idx) 29 | }); 30 | 31 | const delay = (timeInMs: number) => 32 | new Promise(resolve => setTimeout(resolve, timeInMs)); 33 | 34 | const thunks = { 35 | ADD_TODO: (task: string) => async ( 36 | dispatch: Dispatch, 37 | getState: () => State 38 | ) => { 39 | dispatch(actionCreators.CREATE_TODO(task)); 40 | const idx = getState().todos.length - 1; 41 | await delay(1000); 42 | dispatch(actionCreators.MARK_TODO_SAVED(idx)); 43 | } 44 | }; 45 | 46 | export const reducer = createReducer(handler); 47 | export const actionCreators = { 48 | ...createActionCreators(handler), 49 | ...thunks 50 | }; 51 | 52 | export type Dispatch = BoundCreatorsFromActionCreators; 53 | -------------------------------------------------------------------------------- /examples/todomvc/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: 'development', 6 | entry: './src/index.tsx', 7 | devtool: 'source-map', 8 | plugins: [new HtmlWebpackPlugin({ 9 | title: "Typeful Todos", 10 | template: "./src/index.html", 11 | hash: true, 12 | inject: true 13 | })], 14 | output: { 15 | path: path.join(__dirname, '/dist'), 16 | filename: "[name].js", 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.ts', '.tsx'], 20 | modules: ['node_modules'] 21 | }, 22 | module: { 23 | rules: [{ 24 | test: /\.tsx?$/, 25 | loader: 'ts-loader', 26 | exclude: ['node_modules'] 27 | }, { 28 | test: /\.css$/, 29 | loader: ['style-loader', 'css-loader'] 30 | }] 31 | }, 32 | devServer: { 33 | historyApiFallback: true, 34 | disableHostCheck: true, 35 | watchContentBase: false, 36 | watchOptions: { 37 | watch: true 38 | } 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeful-redux", 3 | "version": "0.4.0-alpha-4", 4 | "description": "A typesafe, low boilerplate wrapper for redux to be used in TypeScript projects", 5 | "main": "dist/index.js", 6 | "types": "dist/typeful-redux.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/paulkoerbitz/typeful-redux" 10 | }, 11 | "keywords": [ 12 | "redux", 13 | "react", 14 | "typesafe", 15 | "typescript", 16 | "reducer" 17 | ], 18 | "author": "Paul Koerbitz", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/paulkoerbitz/typeful-redux/issues" 22 | }, 23 | "homepage": "https://github.com/paulkoerbitz/typeful-redux#readme", 24 | "peerDependencies": { 25 | "react": ">=15.0.0", 26 | "react-redux": "5.x", 27 | "redux": "^4.x" 28 | }, 29 | "devDependencies": { 30 | "@types/chai": "^4.0.4", 31 | "@types/jest": "^22.2.2", 32 | "@types/react": "^16.4.11", 33 | "@types/react-dom": "^16.0.7", 34 | "chai": "^4.1.2", 35 | "dts-bundle-generator": "^1.6.1", 36 | "jest": "^22.4.3", 37 | "mocha": "^4.0.1", 38 | "react": "^16.4.2", 39 | "react-dom": "^16.4.2", 40 | "react-redux": "^5.0.7", 41 | "redux": "^4.x", 42 | "redux-thunk": "^2.3.0", 43 | "resolve-types": "^0.2.0", 44 | "ts-jest": "^22.4.2", 45 | "ts-node": "^3.3.0", 46 | "typedoc": "^0.9.0", 47 | "typescript": "^3.0.1", 48 | "webpack": "^4.16.5", 49 | "webpack-serve": "^2.0.2" 50 | }, 51 | "scripts": { 52 | "build:typedef": "dts-bundle-generator -o dist/typeful-redux.d.ts --external-types redux react-redux -- dist/index.d.ts ", 53 | "build": "tsc && npm run build:typedef", 54 | "test": "jest", 55 | "typecheck": "tsc -w --noEmit", 56 | "prepare": "npm run build", 57 | "start:simple-todo": "cd examples/simple-todo && webpack-serve", 58 | "start:todomvc": "cd examples/todomvc && webpack-serve" 59 | }, 60 | "jest": { 61 | "testURL": "http://localhost/", 62 | "transform": { 63 | "^.+\\.tsx?$": "ts-jest" 64 | }, 65 | "testRegex": "(test/.*|src/.*\\.test)\\.tsx?$", 66 | "moduleFileExtensions": [ 67 | "ts", 68 | "tsx", 69 | "js", 70 | "jsx", 71 | "json", 72 | "node" 73 | ] 74 | }, 75 | "dependencies": { 76 | "@types/react-redux": "^6.0.6", 77 | "css-loader": "^1.0.0", 78 | "html-webpack-plugin": "^3.2.0", 79 | "style-loader": "^0.22.1", 80 | "todomvc-app-css": "^2.1.2", 81 | "ts-loader": "^4.5.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/action-creators/index.ts: -------------------------------------------------------------------------------- 1 | import { Arg1, Arg2, Equals, If, Or, ReplaceReturnType } from '../types/helpers'; 2 | import { Dispatch } from '../types/redux'; 3 | import { HandlerMap, ActionFromPayload } from '../handler-map'; 4 | 5 | export const createActionCreators = >( 6 | handlerMap: HM 7 | ): ActionCreatorsFromHandlerMap => { 8 | const result: any = {}; 9 | for (const type in handlerMap) { 10 | if (!handlerMap.hasOwnProperty(type)) { 11 | continue; 12 | } 13 | result[type] = (payload: any) => ({ type, payload }); 14 | } 15 | return result; 16 | }; 17 | 18 | export type ActionCreatorsConstraint = { 19 | [key: string]: ((...xs: any[]) => any); 20 | }; 21 | 22 | export const bindActionCreators = < 23 | ActionCreators extends ActionCreatorsConstraint 24 | >( 25 | actionCreators: ActionCreators, 26 | dispatch: Dispatch> 27 | ): BoundCreatorsFromActionCreators => { 28 | const result = {} as any; 29 | for (const key in actionCreators) { 30 | if (!actionCreators.hasOwnProperty(key)) { 31 | continue; 32 | } 33 | result[key] = (payload: any) => 34 | dispatch((actionCreators as any)[key](payload)); 35 | } 36 | return result; 37 | }; 38 | 39 | export type BoundActionCreatorFromPayload< 40 | ActionName, 41 | P extends ActionName | void | string | object 42 | > = If< 43 | Or, Equals>, 44 | () => void, 45 | (payload: P) => void 46 | >; 47 | 48 | export type ActionCreatorFromPayload< 49 | ActionName, 50 | Payload extends string | void | object 51 | > = If< 52 | Or, Equals>, 53 | () => { type: ActionName }, 54 | (payload: Payload) => { type: ActionName; payload: Payload } 55 | >; 56 | 57 | export type ActionCreatorFromHandlerMapEntry< 58 | ActionName, 59 | HmEntry 60 | > = HmEntry extends (x: any, y: any, ...xs: any[]) => any 61 | ? ActionCreatorFromPayload> 62 | : (() => { type: ActionName }); 63 | 64 | export type ActionCreatorsFromHandlerMap> = { 65 | [ActionName in keyof HM]: ActionCreatorFromHandlerMapEntry< 66 | ActionName, 67 | HM[ActionName] 68 | > 69 | }; 70 | 71 | export type ActionsFromActionCreators< 72 | ActionCreators extends ActionCreatorsConstraint 73 | > = { 74 | [ActionName in keyof ActionCreators]: ReturnType 75 | }[keyof ActionCreators]; 76 | 77 | export type BoundCreatorsFromActionCreators< 78 | ActionCreators extends ActionCreatorsConstraint 79 | > = { 80 | [ActionName in keyof ActionCreators]: ReplaceReturnType< 81 | ActionCreators[ActionName], 82 | void 83 | > 84 | }; 85 | -------------------------------------------------------------------------------- /src/connect/index.ts: -------------------------------------------------------------------------------- 1 | import { Action, Dispatch } from '../types/redux'; 2 | import { connect as redux_connect } from 'react-redux'; 3 | import { Store } from '../store'; 4 | 5 | export interface MapStateToProps { 6 | (state: STATE, ownProps: OWN_PROPS): PROPS_FROM_STATE; 7 | } 8 | 9 | export interface MapDispatchToProps { 10 | (dispatch: DISPATCH, ownProps: OWN_PROPS): PROPS_FROM_DISPATCH; 11 | } 12 | 13 | export interface MergeProps< 14 | PROPS_FROM_STATE, 15 | PROPS_FROM_DISPATCH, 16 | OWN_PROPS, 17 | FINAL_PROPS 18 | > { 19 | ( 20 | stateProps: PROPS_FROM_STATE, 21 | dispatchProps: PROPS_FROM_DISPATCH, 22 | ownProps: OWN_PROPS 23 | ): FINAL_PROPS; 24 | } 25 | 26 | export interface Connect { 27 | < 28 | State, 29 | A extends Action, 30 | OwnProps, 31 | PropsFromState, 32 | PropsFromDispatch 33 | >( 34 | mapStateToProps: MapStateToProps, 35 | mapDispatchToProps: MapDispatchToProps< 36 | Dispatch, 37 | OwnProps, 38 | PropsFromDispatch 39 | > 40 | ): ( 41 | component: React.ComponentClass< 42 | PropsFromState & PropsFromDispatch & OwnProps 43 | > 44 | ) => React.ComponentClass }>; 45 | 46 | < 47 | State, 48 | A extends Action, 49 | OwnProps, 50 | PropsFromState, 51 | PropsFromDispatch, 52 | FinalProps 53 | >( 54 | mapStateToProps: MapStateToProps, 55 | mapDispatchToProps: MapDispatchToProps< 56 | Dispatch, 57 | OwnProps, 58 | PropsFromDispatch 59 | >, 60 | mergeProps: MergeProps< 61 | PropsFromState, 62 | PropsFromDispatch, 63 | OwnProps, 64 | FinalProps 65 | > 66 | ): ( 67 | component: React.ComponentClass 68 | ) => React.ComponentClass }>; 69 | } 70 | 71 | export const connect: Connect = redux_connect; 72 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const INITIAL_STATE_KEY = '___TYPEFUL_REDUX_INTERNAL___INITIAL_STATE___'; -------------------------------------------------------------------------------- /src/handler-map/index.test.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import { createHandlerMap } from '.'; 3 | import { INITIAL_STATE_KEY } from '../constants'; 4 | import { resolveTypes, setOptions } from 'resolve-types'; 5 | import { Diagnostic } from 'typescript'; 6 | 7 | setOptions({ noErrorTruncation: true }); 8 | 9 | const getDiagnosticMessages = (diagnostics: ReadonlyArray) => 10 | diagnostics.map( 11 | d => 12 | typeof d.messageText === 'string' 13 | ? d.messageText 14 | : d.messageText.messageText 15 | ); 16 | 17 | describe('Handler Map', () => { 18 | describe('createHandlerMap runtime', () => { 19 | it(`adds the initial state under the ${INITIAL_STATE_KEY} field`, () => { 20 | const initial = { a: 3, b: 'foo' }; 21 | const hm = createHandlerMap(initial, {}); 22 | expect(hm[INITIAL_STATE_KEY]).toEqual(initial); 23 | }); 24 | 25 | it('returns the handler map unchanged otherwise', () => { 26 | const map = { a: 3, b: 'foo' }; 27 | const hm = createHandlerMap({}, map); 28 | for (const key in map) { 29 | expect(hm[key]).toEqual(map[key]); 30 | } 31 | }); 32 | }); 33 | 34 | const testParameters = [ 35 | { 36 | type: "'foo' | 'bar' | 'quox'", 37 | expectedIn: 'Foo', 38 | expectedOut: 'Foo', 39 | returnTypeAnnotation: ' as Foo', 40 | foo: "'foo'", 41 | bar: "'bar'", 42 | quox: "'quox'" 43 | }, 44 | { 45 | type: 'number[]', 46 | expectedIn: 'number[]', 47 | expectedOut: 'number[]', 48 | foo: '[]', 49 | bar: '[1,2,3]', 50 | quox: '[]' 51 | }, 52 | { 53 | type: '{ a: number; b: string; c: { d: number; }; }', 54 | expectedIn: 'Foo', 55 | expectedOut: 'Partial', 56 | foo: '{ a: 3 }', 57 | bar: '{ a: 5, b: "bar" }', 58 | quox: '{ b: "quox", c: { d: 5 } }' 59 | } 60 | ]; 61 | 62 | for (let { 63 | type, 64 | expectedIn, 65 | expectedOut, 66 | returnTypeAnnotation, 67 | foo, 68 | bar, 69 | quox 70 | } of testParameters) { 71 | returnTypeAnnotation = returnTypeAnnotation || ''; 72 | describe(`createHandlerMap return type for type ${type}`, () => { 73 | const code = ` 74 | import { createHandlerMap, HandlerType, HandlerMap } from './src/handler-map'; 75 | 76 | type Foo = ${type}; 77 | const initialState = ${foo} as Foo; 78 | 79 | const emptyHandlerMap = createHandlerMap(initialState, {}); 80 | type __emptyHandlerMapType = typeof emptyHandlerMap; 81 | 82 | const stringLiteralHandlerMap = createHandlerMap(initialState, { 83 | SET_FOO: ${foo}${returnTypeAnnotation}, 84 | }); 85 | type __stringLiteralHandlerMapType = typeof stringLiteralHandlerMap; 86 | 87 | const nullarySetterHandlerMap = createHandlerMap(initialState, { 88 | SET_BAR: () => (${bar}${returnTypeAnnotation}), 89 | }); 90 | type __nullarySetterHandlerMapType = typeof nullarySetterHandlerMap; 91 | 92 | const unarySetterHandlerMap = createHandlerMap(initialState, { 93 | SET_QUOX: _s => (${foo}${returnTypeAnnotation}) 94 | }); 95 | type __unarySetterHandlerMapType = typeof unarySetterHandlerMap; 96 | 97 | const binarySetterHandlerMap = createHandlerMap(initialState, { 98 | SET_BAAZ: (s, x: number) => (x > 0 ? ${bar} : ${quox}), 99 | }); 100 | type __binarySetterHandlerMapType = typeof binarySetterHandlerMap; 101 | 102 | const allTogetherHandlerMap = createHandlerMap(initialState, { 103 | SET_FOO: ${foo}${returnTypeAnnotation} , 104 | SET_BAR: () => (${bar}${returnTypeAnnotation}), 105 | SET_QUOX: s => (s === ${foo} ? ${bar} : ${quox}), 106 | SET_BAAZ: (s, x: number) => (x > 0 ? ${bar} : ${quox}), 107 | }); 108 | type __allTogetherHandlerMapType = typeof allTogetherHandlerMap; 109 | `; 110 | 111 | const { types, diagnostics } = resolveTypes(code); 112 | 113 | it('works without type errors', () => { 114 | expect(getDiagnosticMessages(diagnostics)).toEqual([]); 115 | }); 116 | 117 | it('is an empty object for an empty handler map', () => { 118 | expect(types['__emptyHandlerMapType']).toEqual('{}'); 119 | }); 120 | 121 | it('has correct literal types for handlers directly providing values', () => { 122 | expect(types['__stringLiteralHandlerMapType']).toEqual( 123 | `{ SET_FOO: ${expectedOut}; }` 124 | ); 125 | }); 126 | 127 | it('has correct types for a nullary setter function', () => { 128 | expect(types['__nullarySetterHandlerMapType']).toEqual( 129 | `{ SET_BAR: () => ${expectedOut}; }` 130 | ); 131 | }); 132 | 133 | it('has correct types for a unary setter function', () => { 134 | expect(types['__unarySetterHandlerMapType']).toEqual( 135 | `{ SET_QUOX: (state: ${expectedIn}) => ${expectedOut}; }` 136 | ); 137 | }); 138 | 139 | it('has correct types for a binary setter function', () => { 140 | expect(types['__binarySetterHandlerMapType']).toEqual( 141 | `{ SET_BAAZ: (state: ${expectedIn}, payload: number) => ${expectedOut}; }` 142 | ); 143 | }); 144 | 145 | it('has correct types for all handlers together', () => { 146 | expect(types['__allTogetherHandlerMapType']).toEqual( 147 | `{ SET_FOO: ${expectedOut}; SET_BAR: () => ${expectedOut}; SET_QUOX: (state: ${expectedIn}) => ${expectedOut}; SET_BAAZ: (state: ${expectedIn}, payload: number) => ${expectedOut}; }` 148 | ); 149 | }); 150 | }); 151 | } 152 | 153 | describe('createHandlerMap creates type errors when', () => { 154 | const testParameters = [ 155 | { 156 | type: "'foo' | 'bar' | 'quox'", 157 | values: { valid: "'foo'", invalid: "'baaz'" }, 158 | typeSynonyms: { 159 | state: ['Foo', "'foo' | 'bar' | 'quox'"], 160 | invalid: ['"baaz"', 'string', '"foo"'] 161 | } 162 | }, 163 | { 164 | type: 'number[]', 165 | values: { valid: '[1, 2]', invalid: "[1, 2, 'bar']" }, 166 | typeSynonyms: { 167 | state: ['number[]'], 168 | invalid: ['(string | number)[]', 'number[]'] 169 | } 170 | }, 171 | { 172 | type: '{ a: number; b: string; c: { d: number; }; }', 173 | values: { 174 | valid: "{ a: 3, b: 'foo', c: { d: 5 } }", 175 | invalid: '{ a: "foo" }' 176 | }, 177 | typeSynonyms: { 178 | state: ['Foo'], 179 | invalid: [ 180 | '{ a: string; }', 181 | '{ a: number; b: string; c: { d: number; }; }' 182 | ] 183 | } 184 | } 185 | ]; 186 | 187 | /** 188 | * Computes all combinations of strings. [['a', 'b'], ['c'], ['x', 'y']] 189 | * gives [['a', 'c', 'x'], ['b', 'c', 'x'], ['a', 'c', 'y'], ['b', 'c', 'y']] 190 | */ 191 | const allCombinations = (xs: string[][]): string[][] => { 192 | if (xs.length <= 0) { 193 | return []; 194 | } 195 | if (xs.length === 1) { 196 | return xs[0].map(x => [x]); 197 | } 198 | const result: string[][] = []; 199 | const subResults = allCombinations(xs.slice(1)); 200 | for (const subResult of subResults) { 201 | for (const x of xs[0]) { 202 | result.push([x, ...subResult]); 203 | } 204 | } 205 | return result; 206 | }; 207 | 208 | const createMessages = ( 209 | msgCreator: (...xs: string[]) => string, 210 | synonyms: string[][] 211 | ) => 212 | allCombinations(synonyms).map(combination => 213 | msgCreator(...combination) 214 | ); 215 | 216 | const expectToEqualWithSynonyms = ( 217 | msg: string, 218 | msgProvider: (...xs: string[]) => string, 219 | synonyms: string[][] 220 | ): void => { 221 | const msgs = createMessages(msgProvider, synonyms); 222 | expect(msgs).toContain(msg); 223 | }; 224 | 225 | for (const { type, values, typeSynonyms } of testParameters) { 226 | const handlerPairs = [ 227 | [`{ SETTER: ${values.invalid} }`, it => `{ SETTER: ${it}; }`], 228 | [ 229 | `{ NULLARY: () => (${values.invalid}) }`, 230 | it => `{ NULLARY: () => ${it}; }` 231 | ], 232 | [ 233 | `{ UNARY: (payload: number) => (${values.invalid}) }`, 234 | it => `{ UNARY: (payload: number) => ${it}; }` 235 | ], 236 | [ 237 | `{ BINARY: (state: ${ 238 | typeSynonyms.state[0] 239 | }, payload: number) => (${values.invalid}) }`, 240 | it => 241 | `{ BINARY: (state: ${ 242 | typeSynonyms.state[0] 243 | }, payload: number) => ${it}; }` 244 | ], 245 | // Invalid state parameter 246 | [ 247 | `{ BINARY: (state: number, payload: number) => (${ 248 | values.valid 249 | }) }`, 250 | it => 251 | `{ BINARY: (state: number, payload: number) => ${it}; }` 252 | ] 253 | ].map( 254 | ([handlerMap, typesCreator]: [string, (x: string) => string]) => 255 | [ 256 | handlerMap, 257 | createMessages(typesCreator, [typeSynonyms.invalid]) 258 | ] as [string, string[]] 259 | ); 260 | 261 | for (const [handlerMap, handlerTypes] of handlerPairs) { 262 | it(`invalid handler map ${handlerMap} for state ${type} is provided`, () => { 263 | const code = ` 264 | import { createHandlerMap, HandlerType } from './src/handler-map'; 265 | 266 | type Foo = ${type}; 267 | const initialState = ${values.valid} as Foo; 268 | 269 | const stringLiteralHandlerMap = createHandlerMap(initialState, ${handlerMap}); 270 | type __wrongHandlerType = typeof stringLiteralHandlerMap; 271 | `; 272 | const message = getDiagnosticMessages( 273 | resolveTypes(code).diagnostics 274 | )[0]; 275 | 276 | const synonyms = [handlerTypes, [type, 'Foo']]; 277 | 278 | const messageCreator = ( 279 | handlerType: string, 280 | stateType: string 281 | ) => 282 | `Argument of type '${handlerType}' is not assignable to parameter of type 'HandlerMapConstraint<${stateType}>'.`; 283 | 284 | expectToEqualWithSynonyms( 285 | message, 286 | messageCreator, 287 | synonyms 288 | ); 289 | }); 290 | } 291 | } 292 | }); 293 | 294 | describe('type operators', () => { 295 | const { types, diagnostics } = resolveTypes` 296 | import { createHandlerMap, StateFromHandlerMap, ActionsFromHandlerMap } from './src/handler-map'; 297 | 298 | const initial = 3 as string | number; 299 | const map = { 300 | FOO: () => 3, 301 | BAR: "hi there", 302 | SET: (payload: number) => payload, 303 | QUOX: (state, payload: number) => payload > 3 ? payload : state 304 | }; 305 | const constructedHm = createHandlerMap(initial, map); 306 | type __StateConstructedHm = StateFromHandlerMap; 307 | 308 | type __StateInferredHm = StateFromHandlerMap; 309 | 310 | type __Actions = ActionsFromHandlerMap; 311 | `; 312 | 313 | describe('StateFromHandlerMap', () => { 314 | it('correctly extracts the type from a given handler map', () => { 315 | expect(getDiagnosticMessages(diagnostics)).toEqual([]); 316 | expect(types['__StateConstructedHm']).toEqual( 317 | 'string | number' 318 | ); 319 | expect(types['__StateInferredHm']).toEqual('any'); 320 | }); 321 | }); 322 | 323 | describe('ActionsFromHandlerMap', () => { 324 | it('extracts the right actions from a given handler map', () => { 325 | expect(types['__Actions']).toEqual("{ type: \"FOO\"; } | { type: \"BAR\"; } | { type: \"SET\"; } | { type: \"QUOX\"; payload: number; }"); 326 | }); 327 | }); 328 | }); 329 | }); 330 | -------------------------------------------------------------------------------- /src/handler-map/index.ts: -------------------------------------------------------------------------------- 1 | import { INITIAL_STATE_KEY } from '../constants'; 2 | import { Arg2, IfArity2, If, Or, Equals } from '../types/helpers'; 3 | 4 | export type NonObjectOrFunction = 5 | | string 6 | | number 7 | | undefined 8 | | null 9 | | any[] 10 | | symbol; 11 | 12 | export type MaybePartial = T extends NonObjectOrFunction ? T : Partial; 13 | 14 | export type HandlerType = T extends NonObjectOrFunction 15 | ? State extends T ? State : never 16 | : T extends (() => MaybePartial) 17 | ? (() => MaybePartial) 18 | : T extends ((state: State) => MaybePartial) 19 | ? (state: State) => MaybePartial 20 | : T extends ((state: State, payload: any) => MaybePartial) 21 | ? ((state: State, payload: Arg2) => MaybePartial) 22 | : T extends MaybePartial ? MaybePartial : never; 23 | 24 | export type HandlerMap = { 25 | [K in keyof HM]: HandlerType 26 | }; 27 | 28 | export type HandlerMapValueConstraint = 29 | | State 30 | | MaybePartial 31 | | (() => MaybePartial) 32 | | ((state: State) => MaybePartial) 33 | | ((state: State, payload: any) => MaybePartial); 34 | 35 | export type HandlerMapConstraint = { 36 | [key: string]: HandlerMapValueConstraint; 37 | }; 38 | 39 | /** 40 | * Type-annotate a map from action names to handling functions 41 | */ 42 | export const createHandlerMap = >( 43 | initialState: State, 44 | handlerMap: HM 45 | ): HandlerMap => { 46 | return { 47 | [INITIAL_STATE_KEY]: initialState, 48 | ...(handlerMap as any) 49 | } as any; 50 | }; 51 | 52 | export type StateFromHandlerMap< 53 | HM extends HandlerMap 54 | > = HM extends HandlerMap ? State : never; 55 | 56 | export type ActionFromPayload< 57 | ActionName, 58 | Payload extends string | void | object 59 | > = If< 60 | Or, Equals>, 61 | { type: ActionName }, 62 | { type: ActionName; payload: Payload } 63 | >; 64 | 65 | export type ActionFromHandlerMapEntry< 66 | ActionName, 67 | HmEntry extends HandlerMapValueConstraint 68 | > = IfArity2< 69 | HmEntry, 70 | ActionFromPayload>, 71 | { type: ActionName } 72 | >; 73 | 74 | export type ActionsFromHandlerMap> = { 75 | [ActionName in keyof HM]: ActionFromHandlerMapEntry< 76 | ActionName, 77 | HM[ActionName] 78 | > 79 | }[keyof HM]; 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { createReducer, Reducer, combineReducers, StateFromReducer, ActionsFromReducer, DispatchFromReducer } from './reducer'; 2 | export { thunk, ThunkMiddleware, ThunkDispatch, createStore, applyMiddleware, Store, StoreCreator, StoreState, StoreActions, ApplyMiddleware } from './store'; 3 | export { connect, Connect } from './connect'; 4 | export { createHandlerMap, HandlerMap } from './handler-map'; 5 | export { createActionCreators, bindActionCreators, BoundCreatorsFromActionCreators } from './action-creators'; 6 | -------------------------------------------------------------------------------- /src/reducer/index.test.ts: -------------------------------------------------------------------------------- 1 | import { resolveTypes } from 'resolve-types'; 2 | import { createHandlerMap, createReducer } from '..'; 3 | 4 | describe('createReducer', () => { 5 | describe('returns a function that', () => { 6 | it('when passed undefined returns the initial state', () => { 7 | const initialState = { a_key: 'a value' }; 8 | const handlerMap = createHandlerMap(initialState, {}); 9 | const reducer = createReducer(handlerMap); 10 | const actual = reducer(undefined, {} as never); 11 | expect(actual).toEqual(initialState); 12 | }); 13 | 14 | it('when passed an existing action invokes the associated handler', () => { 15 | const initialState = { a_key: 'a value' }; 16 | const handlerMap = createHandlerMap(initialState, { 17 | my_action: (s: { a_key: string }, p: string) => { 18 | expect(s.a_key).toEqual('value passed to reducer'); 19 | return { a_key: p }; 20 | } 21 | }); 22 | const reducer = createReducer(handlerMap); 23 | const { a_key: actual } = reducer( 24 | { a_key: 'value passed to reducer' }, 25 | { type: 'my_action', payload: 'expected return value' } 26 | ); 27 | expect(actual).toEqual('expected return value'); 28 | }); 29 | 30 | it('when passed a non-existing action returns the passed-in state', () => { 31 | const initialState = { a_key: 'a value' }; 32 | const handlerMap = { 33 | my_action: (s: { a_key: string }, p: string) => { 34 | expect(s.a_key).toEqual('value passed to reducer'); 35 | return { a_key: p }; 36 | } 37 | }; 38 | const reducer = createReducer( 39 | createHandlerMap(initialState, handlerMap) 40 | ); 41 | const { a_key: actual } = reducer( 42 | { a_key: 'value passed to reducer' }, 43 | { 44 | type: 'unknown_action' as any, 45 | payload: 'not-expected return value' 46 | } 47 | ); 48 | expect(actual).toEqual('value passed to reducer'); 49 | }); 50 | 51 | it('returns a full state even if a handler only provides a partial update', () => { 52 | const initialState = { a: 3, b: 'initial b' }; 53 | const handlerMap = { 54 | foo: { b: 'b set by foo' } 55 | }; 56 | const reducer = createReducer( 57 | createHandlerMap(initialState, handlerMap) 58 | ); 59 | const newState = reducer( 60 | { a: 5, b: 'different b' }, 61 | { type: 'foo' } 62 | ); 63 | expect(newState).toEqual({ a: 5, b: 'b set by foo' }); 64 | }); 65 | 66 | it('correctly handles updates for handlers which are objects', () => { 67 | const initialState = { a: 3, b: 'initial b' }; 68 | const handlerMap = { 69 | foo: { a: -3, b: 'b set by foo' } 70 | }; 71 | const reducer = createReducer( 72 | createHandlerMap(initialState, handlerMap) 73 | ); 74 | const newState = reducer( 75 | { a: 5, b: 'different b' }, 76 | { type: 'foo' } 77 | ); 78 | expect(newState).toEqual({ a: -3, b: 'b set by foo' }); 79 | }); 80 | 81 | it('correctly handles updates for handlers which are nullary functions', () => { 82 | const initialState = { a: 3, b: 'initial b' }; 83 | const handlers = { 84 | foo: () => ({ a: -3, b: 'b set by foo' }), 85 | bar: () => ({ b: 'b set by bar' }) 86 | }; 87 | const handlerMap = createHandlerMap(initialState, handlers); 88 | const reducer = createReducer(handlerMap); 89 | 90 | const stateAfterFoo = reducer( 91 | { a: 5, b: 'different b' }, 92 | { type: 'foo' } 93 | ); 94 | expect(stateAfterFoo).toEqual({ a: -3, b: 'b set by foo' }); 95 | 96 | const stateAfterBar = reducer( 97 | { a: 5, b: 'different b' }, 98 | { type: 'bar' } 99 | ); 100 | expect(stateAfterBar).toEqual({ a: 5, b: 'b set by bar' }); 101 | }); 102 | 103 | it('correctly handles updates for handlers which are unary functions', () => { 104 | const initialState = { a: 3, b: 'initial b' }; 105 | const handlerMap = createHandlerMap(initialState, { 106 | foo: s => ({ a: s.a + 1, b: 'b set by foo' }), 107 | bar: s => ({ b: s.b + ' and bar!' }) 108 | }); 109 | const reducer = createReducer(handlerMap); 110 | 111 | const stateAfterFoo = reducer( 112 | { a: 5, b: 'different b' }, 113 | { type: 'foo' } 114 | ); 115 | expect(stateAfterFoo).toEqual({ a: 6, b: 'b set by foo' }); 116 | 117 | const stateAfterBar = reducer( 118 | { a: 5, b: 'different b' }, 119 | { type: 'bar' } 120 | ); 121 | expect(stateAfterBar).toEqual({ a: 5, b: 'different b and bar!' }); 122 | }); 123 | 124 | it('correctly handles updates for handlers which are binary functions', () => { 125 | const initialState = { a: 3, b: 'initial b' }; 126 | const handlerMap = { 127 | foo: (s, x: number) => ({ a: s.a > 3 ? x : 0 - x, b: 'b set by foo' }), 128 | bar: (s, x: string) => ({ b: s.a > 3 ? '> 3' + x : '<= 3' + x }) 129 | }; 130 | const reducer = createReducer(createHandlerMap(initialState, handlerMap)); 131 | 132 | const stateAfterFoo = reducer( 133 | { a: 5, b: 'different b' }, 134 | { type: 'foo', payload: 10 } 135 | ); 136 | expect(stateAfterFoo).toEqual({ a: 10, b: 'b set by foo' }); 137 | 138 | const stateAfterBar = reducer( 139 | { a: 2, b: 'different b' }, 140 | { type: 'bar', payload: ' hello!!!' } 141 | ); 142 | expect(stateAfterBar).toEqual({ a: 2, b: '<= 3 hello!!!' }); 143 | }); 144 | }); 145 | 146 | describe('infers the right types', () => { 147 | const { 148 | types: { __reducerType } 149 | } = resolveTypes` 150 | import { createReducer, createHandlerMap } from './src'; 151 | const state = {}; 152 | const handlerMap = createHandlerMap(state, {}); 153 | const reducer = createReducer(handlerMap); 154 | type __reducerType = typeof reducer; 155 | `; 156 | 157 | it('has the right type', () => { 158 | expect(__reducerType).toEqual('(state: {}, action: never) => {}'); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /src/reducer/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers as redux_combineReducers } from 'redux'; 2 | import { Action, Dispatch } from '../types/redux'; 3 | import { Arg1, Arg2, If, Equals, IfArity2, NonPartial } from '../types/helpers'; 4 | import { ActionsFromHandlerMap, StateFromHandlerMap } from '../handler-map'; 5 | import { INITIAL_STATE_KEY } from '../constants'; 6 | import { HandlerMapConstraint } from '../handler-map'; 7 | 8 | export type Reducer = ( 9 | state: S | undefined, 10 | action: A 11 | ) => S; 12 | 13 | export const createReducer = >( 14 | handlerMap: HM 15 | ): Reducer, ActionsFromHandlerMap> => { 16 | const initialState = (handlerMap as any)[INITIAL_STATE_KEY]; 17 | return ( 18 | s: StateFromHandlerMap | undefined, 19 | action: ActionsFromHandlerMap 20 | ): StateFromHandlerMap => { 21 | const handler = handlerMap[action.type]; 22 | const oldS = s === undefined ? initialState : s; 23 | let newS = handler; 24 | if (typeof handler === 'function') { 25 | newS = (handler as any)(s, (action as any).payload); 26 | } 27 | if ( 28 | typeof newS === 'number' || 29 | typeof newS === 'string' || 30 | typeof newS === 'boolean' || 31 | typeof newS === 'symbol' || 32 | Array.isArray(newS) 33 | ) { 34 | return (newS as any) as StateFromHandlerMap; 35 | } else if (typeof newS === 'object') { 36 | return { ...(oldS as any), ...(newS as any) }; 37 | } else { 38 | return oldS; 39 | } 40 | }; 41 | }; 42 | 43 | // export interface CombineReducers { 44 | // (reducers: RM): Reducer< 45 | // { [Name in keyof RM]: Arg1 }, 46 | // { [Name in keyof RM]: Arg2 }[keyof RM] 47 | // >; 48 | // } 49 | 50 | export type ReducersMapObject = { [K in keyof S]: Reducer }; 51 | 52 | /** 53 | * Turns an object whose values are different reducer functions, into a single 54 | * reducer function. It will call every child reducer, and gather their results 55 | * into a single state object, whose keys correspond to the keys of the passed 56 | * reducer functions. 57 | * 58 | * @template S Combined state object type. 59 | * 60 | * @param reducers An object whose values correspond to different reducer 61 | * functions that need to be combined into one. One handy way to obtain it 62 | * is to use ES6 `import * as reducers` syntax. The reducers may never 63 | * return undefined for any action. Instead, they should return their 64 | * initial state if the state passed to them was undefined, and the current 65 | * state for any unrecognized action. 66 | * 67 | * @returns A reducer function that invokes every reducer inside the passed 68 | * object, and builds a state object with the same shape. 69 | */ 70 | interface CombineReducers { 71 | (reducers: RM): Reducer< 72 | { [K in keyof RM]: StateFromReducer; }, 73 | { [K in keyof RM]: Arg2 }[keyof RM] 74 | >; 75 | } 76 | 77 | export const combineReducers: CombineReducers = redux_combineReducers; 78 | 79 | export type StateFromReducer> = R extends Reducer< 80 | infer S, 81 | any 82 | > 83 | ? S 84 | : never; 85 | 86 | export type ActionsFromReducer> = R extends Reducer< 87 | any, 88 | infer A 89 | > 90 | ? A 91 | : never; 92 | 93 | /** 94 | * Type of dispatch function that a store created with this 95 | * reducer would have. 96 | */ 97 | export type DispatchFromReducer> = Dispatch< 98 | ActionsFromReducer 99 | >; 100 | -------------------------------------------------------------------------------- /src/store/index.test.ts: -------------------------------------------------------------------------------- 1 | import { createHandlerMap, createReducer, createStore, createActionCreators, combineReducers, bindActionCreators } from '..'; 2 | 3 | describe('createStore', () => { 4 | describe('created stores', () => { 5 | it('an empty store has a \'getState\', \'dispatch\' and \'subscribe\' method', () => { 6 | const store = createStore(createReducer(createHandlerMap(null, {}))); 7 | 8 | expect(typeof store.getState).toEqual('function'); 9 | expect(store.getState.length).toEqual(0); 10 | 11 | expect(typeof store.dispatch).toEqual('function'); 12 | expect(store.dispatch.length).toEqual(1); 13 | 14 | expect(typeof store.subscribe).toEqual('function'); 15 | expect(store.subscribe.length).toEqual(1); 16 | }); 17 | 18 | it('a store with a single simple reducer reduces actions correctly', () => { 19 | const reducer = createReducer(createHandlerMap(0, { 20 | increment: (s: number) => s + 1, 21 | decrement: (s: number) => s - 1, 22 | set: (_s, newValue: number) => newValue 23 | })); 24 | const store = createStore(reducer); 25 | 26 | expect(store.getState()).toEqual(0); 27 | 28 | store.dispatch({ type: "set", payload: 5 }); 29 | expect(store.getState()).toEqual(5); 30 | 31 | store.dispatch({ type: "increment" }); 32 | store.dispatch({ type: "increment" }); 33 | expect(store.getState()).toEqual(7); 34 | 35 | store.dispatch({ type: "decrement" }); 36 | store.dispatch({ type: "decrement" }); 37 | store.dispatch({ type: "decrement" }); 38 | expect(store.getState()).toEqual(4); 39 | 40 | store.dispatch({ type: "set", payload: -2 }); 41 | expect(store.getState()).toEqual(-2); 42 | 43 | }); 44 | 45 | it('a store with a single simple reducer and combineReducers reduces actions correctly', () => { 46 | const handlers = { 47 | increment: (s: number) => s + 1, 48 | decrement: (s: number) => s - 1, 49 | set: (_s: number, newValue: number) => newValue 50 | }; 51 | const handlerMap = createHandlerMap(0, handlers) 52 | const reducer = createReducer(handlerMap); 53 | const store = createStore(combineReducers({ counter: reducer })); 54 | 55 | expect(store.getState().counter).toEqual(0); 56 | 57 | const actionCreators = createActionCreators(handlerMap); 58 | const { increment, decrement, set } = bindActionCreators(actionCreators, store.dispatch); 59 | 60 | set(5); 61 | expect(store.getState().counter).toEqual(5); 62 | 63 | increment(); 64 | increment(); 65 | expect(store.getState().counter).toEqual(7); 66 | 67 | decrement(); 68 | decrement(); 69 | decrement(); 70 | expect(store.getState().counter).toEqual(4); 71 | 72 | set(-2); 73 | expect(store.getState().counter).toEqual(-2); 74 | 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import * as Redux from 'redux'; 2 | import * as Thunk from 'redux-thunk'; 3 | import { Action, Dispatch } from '../types/redux'; 4 | import { Reducer } from '../reducer'; 5 | 6 | /** 7 | * Function to remove listener added by `Store.subscribe()`. 8 | */ 9 | export interface Unsubscribe { 10 | (): void; 11 | } 12 | 13 | /** 14 | * A store is an object that holds the application's state tree. 15 | * There should only be a single store in a Redux app, as the composition 16 | * happens on the reducer level. 17 | * 18 | * @template S The type of state held by this store. 19 | * @template A the type of actions which may be dispatched by this store. 20 | */ 21 | export interface Store< 22 | S, 23 | A extends Action, 24 | D extends Dispatch = Dispatch 25 | > { 26 | dispatch: D; 27 | getState(): S; 28 | subscribe(listener: () => void): Unsubscribe; 29 | replaceReducer(nextReducer: Reducer): void; 30 | } 31 | 32 | export type DeepPartial = { [K in keyof T]?: DeepPartial }; 33 | 34 | /** 35 | * A store creator is a function that creates a Redux store. Like with 36 | * dispatching function, we must distinguish the base store creator, 37 | * `createStore(reducer, preloadedState)` exported from the Redux package, from 38 | * store creators that are returned from the store enhancers. 39 | * 40 | * @template S The type of state to be held by the store. 41 | * @template A The type of actions which may be dispatched. 42 | * @template Ext Store extension that is mixed in to the Store type. 43 | * @template StateExt State extension that is mixed into the state type. 44 | */ 45 | export type StoreCreator< 46 | S, 47 | A extends Action, 48 | D extends Dispatch = Dispatch 49 | > = (reducer: Reducer, preloadedState?: DeepPartial) => Store; 50 | 51 | export type ConcreteStoreCreator = < 52 | S, 53 | A extends Action, 54 | D extends Dispatch = Dispatch 55 | >( 56 | reducer: Reducer, 57 | preloadedState?: DeepPartial 58 | ) => Store; 59 | // = Dispatch>(GenericStoreCreator 60 | 61 | export const createStore = Redux.createStore as ConcreteStoreCreator; 62 | 63 | export type StoreEnhancer< 64 | S, 65 | A extends Action, 66 | D extends Dispatch, 67 | D2 extends Dispatch 68 | > = (storeCreator: StoreCreator) => StoreCreator; 69 | 70 | export interface MiddlewareAPI, S> { 71 | dispatch: D; 72 | getState(): S; 73 | } 74 | 75 | export interface Middleware< 76 | S, 77 | A extends Action, 78 | D extends Dispatch, 79 | D2 extends Dispatch 80 | > { 81 | (api: MiddlewareAPI): (next: D) => D2; 82 | } 83 | 84 | export interface ApplyMiddleware { 85 | // (): StoreEnhancer; 86 | , D2 extends Dispatch>( 87 | middleware1: Middleware 88 | ): StoreEnhancer; 89 | // ( 90 | // middleware1: Middleware, 91 | // middleware2: Middleware 92 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 }>; 93 | // ( 94 | // middleware1: Middleware, 95 | // middleware2: Middleware, 96 | // middleware3: Middleware 97 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 }>; 98 | // ( 99 | // middleware1: Middleware, 100 | // middleware2: Middleware, 101 | // middleware3: Middleware, 102 | // middleware4: Middleware 103 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 }>; 104 | // ( 105 | // middleware1: Middleware, 106 | // middleware2: Middleware, 107 | // middleware3: Middleware, 108 | // middleware4: Middleware, 109 | // middleware5: Middleware 110 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 & Ext5 }>; 111 | // (...middlewares: Middleware[]): StoreEnhancer<{ 112 | // dispatch: Ext; 113 | // }>; 114 | } 115 | 116 | export const applyMiddleware: ApplyMiddleware = Redux.applyMiddleware; 117 | 118 | export type StoreState> = STR extends Store< 119 | infer State, 120 | any 121 | > 122 | ? State 123 | : never; 124 | 125 | export type StoreActions> = STR extends Store< 126 | any, 127 | infer Action 128 | > 129 | ? Action 130 | : never; 131 | 132 | export interface ThunkDispatch { 133 | (action: T): T; 134 | (asyncAction: ThunkAction): R; 135 | } 136 | 137 | export type ThunkAction = ( 138 | dispatch: ThunkDispatch, 139 | getState: () => S, 140 | extraArgument: E 141 | ) => R; 142 | 143 | export interface ThunkMiddleware 144 | extends Middleware, ThunkDispatch> { 145 | withExtraArgument( 146 | extraArgument: E 147 | ): ThunkMiddleware; 148 | } 149 | 150 | // export type ConcreteThunkMiddleware = 151 | 152 | // (api: MiddlewareAPI): ( 153 | // next: D 154 | // ) => D2 withExtraArgument(extraArgument: E): ThunkMiddleware; 155 | // Middleware 156 | // withExtraArgument(extraArgument: E): ThunkMiddleware; 157 | // } 158 | 159 | export const thunk = (Thunk.default as any) as { 160 | , D2 extends Dispatch>( 161 | api: MiddlewareAPI 162 | ): (next: D) => D2; 163 | withExtraArgument: ( 164 | extraArgument: E 165 | ) => ThunkMiddleware; 166 | }; 167 | -------------------------------------------------------------------------------- /src/types/converters.ts: -------------------------------------------------------------------------------- 1 | import { Arg1, Arg2, Equals, If, Or } from './helpers'; 2 | import { HandlerMap, ActionFromPayload } from '../handler-map'; 3 | 4 | 5 | export type ActionCreatorFromPayload< 6 | ActionName, 7 | Payload extends string | void | object 8 | > = If< 9 | Or, Equals>, 10 | () => { type: ActionName }, 11 | (payload: Payload) => { type: ActionName; payload: Payload } 12 | >; 13 | 14 | export type ActionCreatorFromHandlerMapEntry< 15 | ActionName, 16 | HmEntry 17 | > = HmEntry extends (...xs: any[]) => any 18 | ? ActionCreatorFromPayload> 19 | : (() => { type: ActionName }); 20 | 21 | export type ActionCreatorsFromHandlerMap< 22 | State, 23 | HM extends HandlerMap 24 | > = { 25 | [ActionName in keyof HM]: ActionCreatorFromHandlerMapEntry< 26 | ActionName, 27 | HM[ActionName] 28 | > 29 | }; 30 | 31 | export type ActionsFromActionCreators = { 32 | [ActionName in keyof ActionCreators]: ActionFromPayload< 33 | ActionName, 34 | Arg1 35 | > 36 | }[keyof ActionCreators]; 37 | -------------------------------------------------------------------------------- /src/types/helpers.ts: -------------------------------------------------------------------------------- 1 | export type IfArity2 = ((x1: any, x2: any) => any) extends F 2 | ? THEN 3 | : ELSE; 4 | 5 | export type IfArity1 = ((x1: any) => any) extends F 6 | ? THEN 7 | : ELSE; 8 | 9 | export type Arg1 = ((x1: any) => any) extends F 10 | ? F extends ((x1: infer X1, ...xs: any[]) => any) ? X1 : never 11 | : never; 12 | 13 | export type Arg2 = IfArity2< 14 | F, 15 | F extends ((x1: any, x2: infer X2, ...xs: any[]) => any) ? X2 : never, 16 | never 17 | >; 18 | 19 | export type Equals = X extends Y ? (Y extends X ? true : false) : false; 20 | 21 | export type If = Equals< 22 | Test, 23 | true 24 | > extends true 25 | ? Then 26 | : Else; 27 | 28 | export type Or = If< 29 | X, 30 | true, 31 | If 32 | >; 33 | 34 | export type GetKeys = U extends Record ? K : never; 35 | 36 | export type UnionToIntersection = { 37 | [K in GetKeys]: U extends Record ? T : never 38 | }; 39 | 40 | export type NoInfer = T & { [K in keyof T]: T[K] }; 41 | 42 | // Extracted from the follow issue comment 43 | // https://github.com/Microsoft/TypeScript/issues/12936#issuecomment-368244671 44 | // Use this as follows: 45 | // 46 | // type Foo = {a?: string, b: number} 47 | // 48 | // declare function requireExact>(x: X): void; 49 | // 50 | // const exact = {b: 1}; 51 | // requireExact(exact); // okay 52 | // 53 | // const inexact = {a: "hey", b: 3, c: 123}; 54 | // requireExact(inexact); // error 55 | // Types of property 'c' are incompatible. 56 | // Type 'number' is not assignable to type 'never'. 57 | export type Exactify = T & { 58 | [K in keyof X]: K extends keyof T ? X[K] : never 59 | } 60 | 61 | export type NonPartial = { [KEY in keyof T]-?: T[KEY]; }; 62 | 63 | export type GetArgumentsTupleType any> = 64 | F extends ((...rest: infer ARGS) => any) ? ARGS : never; 65 | 66 | export type ReplaceReturnType any, NewReturnType> = 67 | (...xs: GetArgumentsTupleType) => NewReturnType; -------------------------------------------------------------------------------- /src/types/redux.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An *action* is a plain object that represents an intention to change the 3 | * state. Actions are the only way to get data into the store. Any data, 4 | * whether from UI events, network callbacks, or other sources such as 5 | * WebSockets needs to eventually be dispatched as actions. 6 | * 7 | * Actions must have a `type` field that indicates the type of action being 8 | * performed. Types can be defined as constants and imported from another 9 | * module. It's better to use strings for `type` than Symbols because strings 10 | * are serializable. 11 | * 12 | * Other than `type`, the structure of an action object is really up to you. 13 | * If you're interested, check out Flux Standard Action for recommendations on 14 | * how actions should be constructed. 15 | * 16 | * @template T the type of the action's `type` tag. 17 | */ 18 | export interface Action { 19 | type: T; 20 | } 21 | 22 | /** 23 | * An Action type which accepts any other properties. 24 | * This is mainly for the use of the `Reducer` type. 25 | * This is not part of `Action` itself to prevent users who are extending `Action. 26 | */ 27 | export interface AnyAction extends Action { 28 | // Allows any extra properties to be defined in an action. 29 | [extraProps: string]: any; 30 | } 31 | 32 | /* reducers */ 33 | 34 | /** 35 | * A *reducer* (also called a *reducing function*) is a function that accepts 36 | * an accumulation and a value and returns a new accumulation. They are used 37 | * to reduce a collection of values down to a single value 38 | * 39 | * Reducers are not unique to Redux—they are a fundamental concept in 40 | * functional programming. Even most non-functional languages, like 41 | * JavaScript, have a built-in API for reducing. In JavaScript, it's 42 | * `Array.prototype.reduce()`. 43 | * 44 | * In Redux, the accumulated value is the state object, and the values being 45 | * accumulated are actions. Reducers calculate a new state given the previous 46 | * state and an action. They must be *pure functions*—functions that return 47 | * the exact same output for given inputs. They should also be free of 48 | * side-effects. This is what enables exciting features like hot reloading and 49 | * time travel. 50 | * 51 | * Reducers are the most important concept in Redux. 52 | * 53 | * *Do not put API calls into reducers.* 54 | * 55 | * @template S The type of state consumed and produced by this reducer. 56 | * @template A The type of actions the reducer can potentially respond to. 57 | */ 58 | export type Reducer = ( 59 | state: S | undefined, 60 | action: A 61 | ) => S; 62 | 63 | /** 64 | * Object whose values correspond to different reducer functions. 65 | * 66 | * @template A The type of actions the reducers can potentially respond to. 67 | */ 68 | export type ReducersMapObject = { 69 | [K in keyof S]: Reducer 70 | }; 71 | 72 | /** 73 | * Turns an object whose values are different reducer functions, into a single 74 | * reducer function. It will call every child reducer, and gather their results 75 | * into a single state object, whose keys correspond to the keys of the passed 76 | * reducer functions. 77 | * 78 | * @template S Combined state object type. 79 | * 80 | * @param reducers An object whose values correspond to different reducer 81 | * functions that need to be combined into one. One handy way to obtain it 82 | * is to use ES6 `import * as reducers` syntax. The reducers may never 83 | * return undefined for any action. Instead, they should return their 84 | * initial state if the state passed to them was undefined, and the current 85 | * state for any unrecognized action. 86 | * 87 | * @returns A reducer function that invokes every reducer inside the passed 88 | * object, and builds a state object with the same shape. 89 | */ 90 | // export function combineReducers(reducers: ReducersMapObject): Reducer; 91 | // export function combineReducers(reducers: ReducersMapObject): Reducer; 92 | 93 | /* store */ 94 | 95 | /** 96 | * A *dispatching function* (or simply *dispatch function*) is a function that 97 | * accepts an action or an async action; it then may or may not dispatch one 98 | * or more actions to the store. 99 | * 100 | * We must distinguish between dispatching functions in general and the base 101 | * `dispatch` function provided by the store instance without any middleware. 102 | * 103 | * The base dispatch function *always* synchronously sends an action to the 104 | * store's reducer, along with the previous state returned by the store, to 105 | * calculate a new state. It expects actions to be plain objects ready to be 106 | * consumed by the reducer. 107 | * 108 | * Middleware wraps the base dispatch function. It allows the dispatch 109 | * function to handle async actions in addition to actions. Middleware may 110 | * transform, delay, ignore, or otherwise interpret actions or async actions 111 | * before passing them to the next middleware. 112 | * 113 | * @template A The type of things (actions or otherwise) which may be 114 | * dispatched. 115 | */ 116 | export interface Dispatch { 117 | (action: T): T; 118 | } 119 | 120 | /** 121 | * Function to remove listener added by `Store.subscribe()`. 122 | */ 123 | export interface Unsubscribe { 124 | (): void; 125 | } 126 | 127 | /** 128 | * A store is an object that holds the application's state tree. 129 | * There should only be a single store in a Redux app, as the composition 130 | * happens on the reducer level. 131 | * 132 | * @template S The type of state held by this store. 133 | * @template A the type of actions which may be dispatched by this store. 134 | */ 135 | export interface Store { 136 | /** 137 | * Dispatches an action. It is the only way to trigger a state change. 138 | * 139 | * The `reducer` function, used to create the store, will be called with the 140 | * current state tree and the given `action`. Its return value will be 141 | * considered the **next** state of the tree, and the change listeners will 142 | * be notified. 143 | * 144 | * The base implementation only supports plain object actions. If you want 145 | * to dispatch a Promise, an Observable, a thunk, or something else, you 146 | * need to wrap your store creating function into the corresponding 147 | * middleware. For example, see the documentation for the `redux-thunk` 148 | * package. Even the middleware will eventually dispatch plain object 149 | * actions using this method. 150 | * 151 | * @param action A plain object representing “what changed”. It is a good 152 | * idea to keep actions serializable so you can record and replay user 153 | * sessions, or use the time travelling `redux-devtools`. An action must 154 | * have a `type` property which may not be `undefined`. It is a good idea 155 | * to use string constants for action types. 156 | * 157 | * @returns For convenience, the same action object you dispatched. 158 | * 159 | * Note that, if you use a custom middleware, it may wrap `dispatch()` to 160 | * return something else (for example, a Promise you can await). 161 | */ 162 | dispatch: Dispatch; 163 | 164 | /** 165 | * Reads the state tree managed by the store. 166 | * 167 | * @returns The current state tree of your application. 168 | */ 169 | getState(): S; 170 | 171 | /** 172 | * Adds a change listener. It will be called any time an action is 173 | * dispatched, and some part of the state tree may potentially have changed. 174 | * You may then call `getState()` to read the current state tree inside the 175 | * callback. 176 | * 177 | * You may call `dispatch()` from a change listener, with the following 178 | * caveats: 179 | * 180 | * 1. The subscriptions are snapshotted just before every `dispatch()` call. 181 | * If you subscribe or unsubscribe while the listeners are being invoked, 182 | * this will not have any effect on the `dispatch()` that is currently in 183 | * progress. However, the next `dispatch()` call, whether nested or not, 184 | * will use a more recent snapshot of the subscription list. 185 | * 186 | * 2. The listener should not expect to see all states changes, as the state 187 | * might have been updated multiple times during a nested `dispatch()` before 188 | * the listener is called. It is, however, guaranteed that all subscribers 189 | * registered before the `dispatch()` started will be called with the latest 190 | * state by the time it exits. 191 | * 192 | * @param listener A callback to be invoked on every dispatch. 193 | * @returns A function to remove this change listener. 194 | */ 195 | subscribe(listener: () => void): Unsubscribe; 196 | 197 | /** 198 | * Replaces the reducer currently used by the store to calculate the state. 199 | * 200 | * You might need this if your app implements code splitting and you want to 201 | * load some of the reducers dynamically. You might also need this if you 202 | * implement a hot reloading mechanism for Redux. 203 | * 204 | * @param nextReducer The reducer for the store to use instead. 205 | */ 206 | replaceReducer(nextReducer: Reducer): void; 207 | } 208 | 209 | // export type DeepPartial = { [K in keyof T]?: DeepPartial }; 210 | 211 | /** 212 | * A store creator is a function that creates a Redux store. Like with 213 | * dispatching function, we must distinguish the base store creator, 214 | * `createStore(reducer, preloadedState)` exported from the Redux package, from 215 | * store creators that are returned from the store enhancers. 216 | * 217 | * @template S The type of state to be held by the store. 218 | * @template A The type of actions which may be dispatched. 219 | * @template Ext Store extension that is mixed in to the Store type. 220 | * @template StateExt State extension that is mixed into the state type. 221 | */ 222 | export interface StoreCreator { 223 | ( 224 | reducer: Reducer, 225 | enhancer: StoreEnhancer 226 | ): Store & Ext; 227 | // ( 228 | // reducer: Reducer, 229 | // preloadedState: DeepPartial, 230 | // enhancer?: StoreEnhancer 231 | // ): Store & Ext; 232 | } 233 | 234 | /** 235 | * Creates a Redux store that holds the state tree. 236 | * The only way to change the data in the store is to call `dispatch()` on it. 237 | * 238 | * There should only be a single store in your app. To specify how different 239 | * parts of the state tree respond to actions, you may combine several 240 | * reducers 241 | * into a single reducer function by using `combineReducers`. 242 | * 243 | * @template S State object type. 244 | * 245 | * @param reducer A function that returns the next state tree, given the 246 | * current state tree and the action to handle. 247 | * 248 | * @param [preloadedState] The initial state. You may optionally specify it to 249 | * hydrate the state from the server in universal apps, or to restore a 250 | * previously serialized user session. If you use `combineReducers` to 251 | * produce the root reducer function, this must be an object with the same 252 | * shape as `combineReducers` keys. 253 | * 254 | * @param [enhancer] The store enhancer. You may optionally specify it to 255 | * enhance the store with third-party capabilities such as middleware, time 256 | * travel, persistence, etc. The only store enhancer that ships with Redux 257 | * is `applyMiddleware()`. 258 | * 259 | * @returns A Redux store that lets you read the state, dispatch actions and 260 | * subscribe to changes. 261 | */ 262 | // export const createStore: StoreCreator; 263 | 264 | /** 265 | * A store enhancer is a higher-order function that composes a store creator 266 | * to return a new, enhanced store creator. This is similar to middleware in 267 | * that it allows you to alter the store interface in a composable way. 268 | * 269 | * Store enhancers are much the same concept as higher-order components in 270 | * React, which are also occasionally called “component enhancers”. 271 | * 272 | * Because a store is not an instance, but rather a plain-object collection of 273 | * functions, copies can be easily created and modified without mutating the 274 | * original store. There is an example in `compose` documentation 275 | * demonstrating that. 276 | * 277 | * Most likely you'll never write a store enhancer, but you may use the one 278 | * provided by the developer tools. It is what makes time travel possible 279 | * without the app being aware it is happening. Amusingly, the Redux 280 | * middleware implementation is itself a store enhancer. 281 | * 282 | * @template Ext Store extension that is mixed into the Store type. 283 | * @template StateExt State extension that is mixed into the state type. 284 | */ 285 | export type StoreEnhancer = ( 286 | next: StoreEnhancerStoreCreator<{}, {}, S, A> 287 | ) => StoreEnhancerStoreCreator; 288 | 289 | export type StoreEnhancerStoreCreator = ( 290 | reducer: Reducer 291 | ) => // , 292 | // preloadedState?: DeepPartial 293 | Store & Ext; 294 | 295 | /* middleware */ 296 | 297 | export interface MiddlewareAPI { 298 | dispatch: D; 299 | getState(): S; 300 | } 301 | 302 | /** 303 | * A middleware is a higher-order function that composes a dispatch function 304 | * to return a new dispatch function. It often turns async actions into 305 | * actions. 306 | * 307 | * Middleware is composable using function composition. It is useful for 308 | * logging actions, performing side effects like routing, or turning an 309 | * asynchronous API call into a series of synchronous actions. 310 | * 311 | * @template DispatchExt Extra Dispatch signature added by this middleware. 312 | * @template S The type of the state supported by this middleware. 313 | * @template D The type of Dispatch of the store where this middleware is 314 | * installed. 315 | */ 316 | // export interface Middleware< 317 | // DispatchExt, 318 | // S, 319 | // A extends Action, 320 | // D extends Dispatch & Dispatch 321 | // > { 322 | // (api: MiddlewareAPI): ( 323 | // next: Dispatch 324 | // ) => (action: any) => any; 325 | // } 326 | 327 | /** 328 | * Creates a store enhancer that applies middleware to the dispatch method 329 | * of the Redux store. This is handy for a variety of tasks, such as 330 | * expressing asynchronous actions in a concise manner, or logging every 331 | * action payload. 332 | * 333 | * See `redux-thunk` package as an example of the Redux middleware. 334 | * 335 | * Because middleware is potentially asynchronous, this should be the first 336 | * store enhancer in the composition chain. 337 | * 338 | * Note that each middleware will be given the `dispatch` and `getState` 339 | * functions as named arguments. 340 | * 341 | * @param middlewares The middleware chain to be applied. 342 | * @returns A store enhancer applying the middleware. 343 | * 344 | * @template Ext Dispatch signature added by a middleware. 345 | * @template S The type of the state supported by a middleware. 346 | */ 347 | // export interface ApplyMiddleware { 348 | // // (): StoreEnhancer; 349 | // & Dispatch>( 350 | // middleware1: Middleware 351 | // ): StoreEnhancer< 352 | // { 353 | // dispatch: Ext1; 354 | // }, 355 | // {}, 356 | // S, 357 | // A 358 | // >; 359 | // ( 360 | // middleware1: Middleware, 361 | // middleware2: Middleware 362 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 }>; 363 | // ( 364 | // middleware1: Middleware, 365 | // middleware2: Middleware, 366 | // middleware3: Middleware 367 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 }>; 368 | // ( 369 | // middleware1: Middleware, 370 | // middleware2: Middleware, 371 | // middleware3: Middleware, 372 | // middleware4: Middleware 373 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 }>; 374 | // ( 375 | // middleware1: Middleware, 376 | // middleware2: Middleware, 377 | // middleware3: Middleware, 378 | // middleware4: Middleware, 379 | // middleware5: Middleware 380 | // ): StoreEnhancer<{ dispatch: Ext1 & Ext2 & Ext3 & Ext4 & Ext5 }>; 381 | // (...middlewares: Middleware[]): StoreEnhancer<{ 382 | // dispatch: Ext; 383 | // }>; 384 | // } 385 | 386 | /* action creators */ 387 | 388 | /** 389 | * An *action creator* is, quite simply, a function that creates an action. Do 390 | * not confuse the two terms—again, an action is a payload of information, and 391 | * an action creator is a factory that creates an action. 392 | * 393 | * Calling an action creator only produces an action, but does not dispatch 394 | * it. You need to call the store's `dispatch` function to actually cause the 395 | * mutation. Sometimes we say *bound action creators* to mean functions that 396 | * call an action creator and immediately dispatch its result to a specific 397 | * store instance. 398 | * 399 | * If an action creator needs to read the current state, perform an API call, 400 | * or cause a side effect, like a routing transition, it should return an 401 | * async action instead of an action. 402 | * 403 | * @template A Returned action type. 404 | */ 405 | // export interface ActionCreator { 406 | // (...args: any[]): A; 407 | // } 408 | 409 | // /** 410 | // * Object whose values are action creator functions. 411 | // */ 412 | // export interface ActionCreatorsMapObject { 413 | // [key: string]: ActionCreator; 414 | // } 415 | 416 | // // import { Middleware, Action, AnyAction } from "redux"; 417 | 418 | // export interface ThunkDispatch { 419 | // (action: T): T; 420 | // (asyncAction: ThunkAction): R; 421 | // } 422 | 423 | // export type ThunkAction = ( 424 | // dispatch: ThunkDispatch, 425 | // getState: () => S, 426 | // extraArgument: E 427 | // ) => R; 428 | 429 | // // export interface ThunkMiddleware { 430 | // // Middleware, S, A, ThunkDispatch >; 431 | // withExtraArgument(extraArgument: E): ThunkMiddleware<{}, A, E>; 432 | // }; 433 | 434 | // declare const thunk: ThunkMiddleware; 435 | 436 | // export default thunk; 437 | -------------------------------------------------------------------------------- /test/types/ts-tests.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | import { resolveTypes } from 'resolve-types'; 3 | 4 | describe.skip('Type Operators', () => { 5 | describe('Arg1', () => { 6 | const { 7 | types: { __1, __2, __3, __4 } 8 | } = resolveTypes` 9 | import * as TH from './src/type-helpers'; 10 | type __1 = TH.Arg1<(x: string) => void>; 11 | type __2 = TH.Arg1<(x: string, y: number) => void>; 12 | type __3 = TH.Arg1<{ x: string, y: number }>; 13 | type __4 = TH.Arg1<() => void>; 14 | `; 15 | 16 | it('extracts first type argument of a univariate function', () => { 17 | expect(__1).toEqual('string'); 18 | }); 19 | 20 | it('extracts first type argument of a bivariate function', () => { 21 | expect(__2).toEqual('string'); 22 | }); 23 | 24 | it("returns 'never' if the argument is not a function", () => { 25 | expect(__3).toEqual('never'); 26 | }); 27 | 28 | it("returns 'never' if the argument is a nullary function", () => { 29 | expect(__4).toEqual('never'); 30 | }); 31 | }); 32 | 33 | describe('Arg2', () => { 34 | const { types: { __1, __2, __3, __4, __5 } } = resolveTypes` 35 | import * as TH from './src/type-helpers'; 36 | type __1 = TH.Arg2<(x: string, y: number) => void>; 37 | type __2 = TH.Arg2<(x: string) => void>; 38 | type __3 = TH.Arg2<(x: string, y: number, z: {}) => void>; 39 | type __4 = TH.Arg2<() => void>; 40 | type __5 = TH.Arg2<{ x: string; y: number }>; 41 | `; 42 | 43 | it('returns the type of the second argument for a binary function', () => { 44 | expect(__1).toEqual('number'); 45 | }); 46 | 47 | it('returns never for a uniary function', () => { 48 | expect(__2).toEqual('never'); 49 | }); 50 | 51 | it('returns the type of the second argument for a trinary function', () => { 52 | expect(__3).toEqual('number'); 53 | }); 54 | 55 | it('returns never for a nullary function', () => { 56 | expect(__4).toEqual('never'); 57 | }); 58 | 59 | it('returns never for a non-function', () => { 60 | expect(__5).toEqual('never'); 61 | }); 62 | }); 63 | }); 64 | 65 | describe.skip('Type Converters', () => { 66 | describe('ActionsFromHandlerMap', () => { 67 | it('extracts a simple action', () => { 68 | const { types: { __1 } } = resolveTypes` 69 | import { ActionsFromHandlerMap } from './src/type-converters'; 70 | type __1 = ActionsFromHandlerMap<{ Action1: (state: string[], payload: string) => string[]; }>; 71 | `; 72 | expect(__1).toEqual('{ type: "Action1"; payload: string; }'); 73 | }); 74 | }); 75 | 76 | describe('ActionCreatorsFromHandlerMap', () => {}); 77 | }); 78 | -------------------------------------------------------------------------------- /test/types/type-helpers.ts: -------------------------------------------------------------------------------- 1 | import { resolveTypes } from 'resolve-types'; 2 | 3 | describe.skip('type-helpers', () => { 4 | describe('Arg1', () => { 5 | const { 6 | types: { __1, __2, __3, __4 }, 7 | diagnostics 8 | } = resolveTypes` 9 | import { Arg1 } from './src/type-helpers'; 10 | type ${1} = Arg1<(x: string) => void>; 11 | type ${2} = Arg1<(x: number, y: string) => void>; 12 | type ${3} = Arg1<() => void>; 13 | type ${4} = Arg1; 14 | `; 15 | 16 | it('correctly infers the first argument type of a unary function', () => { 17 | expect(diagnostics).toEqual([]); 18 | expect(__1).toEqual('string'); 19 | }); 20 | 21 | it('correctly infers the first argument type of a binary function', () => { 22 | expect(diagnostics).toEqual([]); 23 | expect(__2).toEqual('number'); 24 | }); 25 | 26 | it('returns type "never" for a nullary function', () => { 27 | expect(diagnostics).toEqual([]); 28 | expect(__3).toEqual('never'); 29 | }); 30 | 31 | it('returns type "never" for a non-function', () => { 32 | expect(diagnostics).toEqual([]); 33 | expect(__4).toEqual('never'); 34 | }); 35 | }); 36 | 37 | describe('Arg2', () => { 38 | const { 39 | types: { __1, __2, __3, __4, __5 }, 40 | diagnostics 41 | } = resolveTypes` 42 | import { Arg2 } from './src/type-helpers'; 43 | type ${1} = Arg2<(x: string, y: { foo: string; }) => void>; 44 | type ${2} = Arg2<(x: number, y: { bar: number; }, z: string) => void>; 45 | type ${3} = Arg2<(x: any) => void>; 46 | type ${4} = Arg2<() => void>; 47 | type ${5} = Arg2; 48 | `; 49 | 50 | it('correctly infers the second argument type of a binary function', () => { 51 | expect(diagnostics).toEqual([]); 52 | expect(__1).toEqual('{ foo: string; }'); 53 | }); 54 | 55 | it('correctly infers the second argument type of a trinary function', () => { 56 | expect(diagnostics).toEqual([]); 57 | expect(__2).toEqual('{ bar: number; }'); 58 | }); 59 | 60 | it('returns type "never" for a unary function', () => { 61 | expect(diagnostics).toEqual([]); 62 | expect(__3).toEqual('never'); 63 | }); 64 | 65 | it('returns type "never" for a nullary function', () => { 66 | expect(diagnostics).toEqual([]); 67 | expect(__4).toEqual('never'); 68 | }); 69 | 70 | it('returns type "never" for a non-function', () => { 71 | expect(diagnostics).toEqual([]); 72 | expect(__5).toEqual('never'); 73 | }); 74 | }); 75 | 76 | describe('Equals', () => { 77 | const { 78 | types: { __1, __2 }, 79 | diagnostics 80 | } = resolveTypes` 81 | import { Equals } from './src/type-helpers'; 82 | type ${1} = Equals; 83 | type ${2} = Equals; 84 | `; 85 | 86 | it('Equals resolves to true', () => { 87 | expect(diagnostics).toEqual([]); 88 | expect(__1).toEqual('true'); 89 | }); 90 | 91 | it('Equals resolves to false', () => { 92 | expect(diagnostics).toEqual([]); 93 | expect(__2).toEqual('false'); 94 | }); 95 | }) 96 | }); 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | /* Strict Type-Checking Options */ 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | /* Additional Checks */ 14 | /* "noUnusedLocals": true, */ 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noErrorTruncation": true, 19 | "outDir": "dist/", 20 | "jsx": "react", 21 | "lib": ["es5", "dom", "es2015.promise"] 22 | }, 23 | "files": [ 24 | "src/index.ts", 25 | "examples/todomvc/src/index.tsx" 26 | ] 27 | } 28 | --------------------------------------------------------------------------------