├── .all-contributorsrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .nvmrc ├── .travis.yml ├── LICENSE ├── README.md ├── code-of-conduct.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── operators │ ├── as-unknown-error.test.ts │ ├── as-unknown-error.ts │ ├── catch-error.test.ts │ ├── catch-error.ts │ ├── chain.test.ts │ ├── chain.ts │ ├── index.ts │ ├── map.test.ts │ └── map.ts └── task │ ├── index.ts │ ├── task-all.test.ts │ ├── task-all.ts │ ├── task-from-promise.test.ts │ ├── task-from-promise.ts │ ├── task-operators.ts │ ├── task-pipe.test.ts │ ├── task-pipe.ts │ ├── task.test.ts │ ├── task.ts │ ├── unknown-error.test.ts │ └── unknown-error.ts ├── test ├── jest-helper.ts └── types │ ├── basic-usage.ts │ ├── index.d.ts │ ├── operators │ ├── all.ts │ ├── from-promise.ts │ └── pipe.ts │ ├── parametrize-reject-and-resolve.ts │ ├── tsconfig.json │ └── tslint.json ├── tools ├── gh-pages-publish.ts └── semantic-release-prepare.ts ├── tsconfig.json └── tslint.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "@ts-task/task", 3 | "projectOwner": "hrajchert", 4 | "files": [ 5 | "README.md" 6 | ], 7 | "imageSize": 100, 8 | "commit": true, 9 | "contributors": [ 10 | { 11 | "login": "hrajchert", 12 | "name": "Hernan Rajchert", 13 | "avatar_url": "https://avatars0.githubusercontent.com/u/2634059?v=4", 14 | "profile": "https://github.com/hrajchert", 15 | "contributions": [ 16 | "code", 17 | "design", 18 | "doc", 19 | "example", 20 | "test" 21 | ] 22 | }, 23 | { 24 | "login": "dggluz", 25 | "name": "Gonzalo Gluzman", 26 | "avatar_url": "https://avatars1.githubusercontent.com/u/1573956?v=4", 27 | "profile": "https://github.com/dggluz", 28 | "contributions": [ 29 | "code", 30 | "ideas", 31 | "test" 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 4 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .npmrc 12 | .rpt2_cache 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | dist/lib/test/* 4 | dist/docs/* 5 | .nyc_output 6 | .DS_Store 7 | *.log 8 | .vscode 9 | .idea 10 | compiled 11 | src 12 | tools 13 | dist/docs 14 | .awcache 15 | tslint.json 16 | rollup.config.ts 17 | .travis.yml 18 | .editorconfig 19 | .all-contributorsrc 20 | .test.* 21 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v10.1.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | branches: 3 | only: 4 | - master 5 | - /^greenkeeper/.*$/ 6 | cache: 7 | yarn: true 8 | directories: 9 | - node_modules 10 | notifications: 11 | email: true 12 | node_js: 13 | - node 14 | script: 15 | - npm run test:prod && npm run build 16 | after_success: 17 | - npm run report-coverage 18 | - npm run deploy-docs 19 | - npm run semantic-release 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 TS-TASK 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Npm version](https://img.shields.io/npm/v/@ts-task/task.svg)](https://www.npmjs.com/package/@ts-task/task) 2 | [![Build Status](https://travis-ci.com/ts-task/task.svg?branch=master)](https://travis-ci.com/ts-task/task) 3 | [![Coverage Status](https://coveralls.io/repos/github/ts-task/task/badge.svg?branch=master)](https://coveralls.io/github/ts-task/task?branch=master) 4 | ![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/@ts-task/task.svg) 5 | ![Created in acamica labs](https://img.shields.io/badge/created%20in-AcamicaLabs-blue.svg) 6 | 7 | 8 | # Task 9 | 10 | It's Like a Promise with typed errors and other improvements that leads to more robust code. 11 | 12 | > made (and best used) with [TypeScript](http://www.typescriptlang.org/) ♥️ 13 | 14 | ### Usage 15 | **Install** the library in your project. 16 | ```bash 17 | npm install @ts-task/task 18 | ``` 19 | 20 | **Use** it in your code preeeeety much how you would use a Promise. 21 | 22 | ```typescript 23 | import { Task } from '@ts-task/task'; 24 | 25 | // Create it with a resolver 26 | const task1 = new Task((resolve, reject) => { 27 | setTimeout( 28 | _ => resolve('Hello'), 29 | 2000 30 | ) 31 | }) 32 | 33 | // Or with a constructor 34 | const task2 = Task.resolve('world'); 35 | 36 | Task.all([task1, task2]) 37 | // Transform the eventual value 38 | .map(([msg1, msg2]) => `${msg1} ${msg2}!!!`) 39 | // And then do something with it 40 | .fork( 41 | err => console.error('Buu', err), // Errors come first! 42 | msg => console.log('Yeay', msg) 43 | ); 44 | ``` 45 | 46 | ### Why 47 | 48 | Promises are great! so why do we need a replacement? or when should I use `Task`? 49 | 50 | Good question, I'm happy you asked: 51 | 52 | * Task have [better error handling](#better-error-handling), so you'll have less bugs 53 | * Task are [`pipe`able](#pipe-operator), so they are easier to extend 54 | * Task has more [specific semantics](#specific-semantics), so it will be easier to know what you are doing 55 | * Task are Lazy, so it's easier to create retry logic 56 | 57 | ### Better Error Handling 58 | If you ever used Promises in TypeScript you may have noticed that it's only typed on success a.k.a `Promise`. For example in the following code 59 | 60 | ```typescript 61 | const somePromise: Promise; 62 | 63 | somePromise 64 | // Transform the eventual value 65 | .then(x => `${x}!!!`) 66 | // And then do something with it 67 | .then( 68 | value => /* value is of type string */, 69 | err => /* err is of type any */ 70 | ) 71 | ``` 72 | 73 | We know `value` is of the expected type `T` but we don't know **any**thing about `err`. The main reason is that when we transform our promises, the callbacks we pass to `then` can throw **any**thing, and in TypeScript the exceptions are not typed. We could manually define the error type and say it's a `Promise` but it would be a lie because we can't avoid exceptions and if they happen we can't know their types, and because `any & Error = any` we can't be more specific. 74 | 75 | Thats a boomer because we make all this trouble with static typings to have more confidence on how we program and we are left wide open when things go south. 76 | 77 | 78 | So we can't forbid a function from throwing but we can wrap exceptions inside an `UnknownError` object, and with that decision alone we can type `Task` and let TypeScript help us infer and manipulate errors 🎉. 79 | 80 | For example 81 | 82 | ```typescript 83 | const task1 = Task.resolve(1); 84 | // task1 is of type Task, which makes sense as there is no way resolve can fail 85 | 86 | const task2 = task1.map(n => '' + n) 87 | // task2 is of type Task, because we have converted the success value 88 | // and we don't know if the inner function throws an error or not 89 | ``` 90 | 91 | You can also add, remove and transform your error logic and the type inference will get you a long way. 92 | 93 | For example if you use the `caseError` function from [@ts-task/utils](https://github.com/ts-task/utils) you could do something like this 94 | 95 | ```typescript 96 | import { Task } from 'ts-task/task'; 97 | import { caseError, isInstanceOf } from 'ts-task/utils'; 98 | 99 | // Assuming we have defined a getUser function somewhere 100 | declare function getUser(id: number): Task; 101 | 102 | const user = getUser(100) 103 | .catch( 104 | caseError( 105 | // If the error meets this condition 106 | isInstanceOf(UserNotFound), 107 | // Handle it with this callback 108 | err => Task.resolve(new Guest()) 109 | ) 110 | ) 111 | // user will have type Task because caseError only handles 112 | // UserNotFound (removing it from the errors) and resolves it to a new type of answer (Guest) 113 | // and there is always the possibility that one of those functions throws, so we have to take UnknownError 114 | // into account 115 | ``` 116 | 117 | Another use case could be to only retry an http request if the error was 502, or 504 which may happen on a timely basis but don't retry if the error was 401 or a rate limit as the expected result is the same. 118 | 119 | When you fork a Task, the error callback comes first, so whenever you want to use the eventual value, you first need to decide what you do with the error. You can always ignore it or `console.log` it, but you need to make a conscious decision. 120 | 121 | ```typescript 122 | Task 123 | .resolve('Hello!') 124 | .fork( 125 | err => console.error('Buu', err), // Errors come first! 126 | msg => console.log('Yeay', msg) 127 | ); 128 | ``` 129 | 130 | 131 | ### Pipe operator 132 | When the [pipe operator](https://github.com/tc39/proposal-pipeline-operator) lands to JavaScript (currently in stage 1) we will be able to write code like this 133 | 134 | ```javascript 135 | const task = Task.resolve(1) 136 | |> map(n => '' + n) 137 | |> chain(getUser) 138 | |> retryWhen(DbError) 139 | |> catchError(caseError(UserNotFound, err => Task.resolve(null))) 140 | ``` 141 | 142 | which has the advantage of using custom methods without having to modify the prototype of `Task`. But because we cannot 143 | wait until the operator makes it to the standard we added a `pipe` method inspired by [RxJs pipeable operators](https://github.com/ReactiveX/rxjs/blob/master/doc/pipeable-operators.md). 144 | 145 | So the previous code would look like: 146 | 147 | ```javascript 148 | const task = Task.resolve(1).pipe( 149 | map(n => '' + n) 150 | , chain(getUser) 151 | , retryWhen(DbError) 152 | , catchError(caseError(UserNotFound, err => Task.resolve(null))) 153 | ) 154 | ``` 155 | 156 | which is not that different. All that's required is that the functions passed pipe to have the signature 157 | `Task => Task`. You can find a common operators in the [@ts-task/utils](https://github.com/ts-task/utils) library, but we encourage you to write your own. 158 | 159 | ### Specific semantics 160 | 161 | Promises API is quite simple by design, it has a `then` method that can be used for 3 different purposes, in contrast *Task* has a different method for each usage. 162 | 163 | * `Promise.then` can be used to **transform** an eventual value, with *task* you should use `map`. 164 | * `Promise.then` can be used to **chain** sequential async operations, with *task* you should use `chain`. 165 | * `Promise.then` can be used to **do** something once you have the result, with *task* you should use `fork`. 166 | 167 | As stated in *Lord of the Promises* 168 | > One `method` to rule them all, One `method` to find them, 169 | > One `method` to bring them all and in the darkness **bind** them 170 | 171 | The nice thing about having one method should be simplicity (less methods to remember), but trying to put the different use cases in the same method can cause some confusions that we'll explain in this section. 172 | 173 | When we want to **do** something with an eventual value, we need to know if the Promise *succeeds* or *fails*. Thats why `then` accepts two arguments, the *onSuccess* and *onError* callbacks (in that order). 174 | 175 | If it's used in the middle of a Promise chain, it can cause some confusion 176 | 177 | ```javascript 178 | // It's not recommended to do this 179 | somePromise 180 | .then(x => foo(x)) 181 | .then( 182 | y => bar(y), 183 | err => handleError(err) 184 | ) 185 | .then(z => baz(z)) 186 | ``` 187 | 188 | a common doubt arises with `handleError`, does it catch errors on *foo* or in *bar*?. The answer is the first option, thats why it's recommended to write an explicit `catch` instead. 189 | 190 | ```javascript 191 | // Instead do this 192 | somePromise 193 | .then(x => foo(x)) 194 | .catch(err => handleError(err)) 195 | .then(y => bar(y)) 196 | .then(z => baz(z)) 197 | ``` 198 | 199 | But just the fact that you can write the previous code can be misleading. 200 | 201 | The second argument of `then` should only be used in the last step of a Promise chain, when we are **do**ing something with the result. 202 | 203 | ```javascript 204 | somePromise 205 | .then(...) 206 | .then(...) 207 | .then( 208 | html => render(html), 209 | err => openErrorModal(err) 210 | ); 211 | ``` 212 | 213 | But because the second parameter is optional, it's fairly easy to end up with fragile code. For example: 214 | 215 | ```javascript 216 | somePromise 217 | .then(...) 218 | .then(...) 219 | .then( 220 | html => render(html) 221 | ); 222 | ``` 223 | 224 | if `somePromise` fails there is no handler, depending on the environment you could get a silent error or an `Uncaught Promise Rejection` that may be difficult to trace. 225 | 226 | When using task, if you want to **do** something with your eventual result you have to use `fork` as the last step. That method is the only one that doesn't return a new *task*, so it's impossible to use it in the middle of the chain. Even more, `fork` handles errors in the first callback, so it's impossible to have an `Uncaught Promise Rejection`. 227 | 228 | ```javascript 229 | someTask 230 | .map(...) 231 | .chain(...) 232 | .fork( 233 | err => openErrorModal(err), 234 | html => render(html) 235 | ) 236 | ``` 237 | 238 | And because *Tasks* are lazy, if you don't call `fork` nothing happens, so the library forces you to use best practices. 239 | 240 | The difference between `map` and `chain` is a little more subtle. Promise (and Tasks) **transform**ations are useful when you don't care about the eventual value itself, rather something that can be synchronously computed from that value. For example, you could fetch a document and only care about how many words the document has. 241 | 242 | ```javascript 243 | fetch('http://task-manifesto.org') 244 | .then(doc => countWords(doc)) 245 | .then(n => alert(`The document has ${n} words`)); 246 | 247 | fetchTask('http://task-manifesto.org') 248 | .map(doc => countWords(doc)) 249 | .fork( 250 | noop, 251 | n => alert(`The document has ${n} words`) 252 | ); 253 | ``` 254 | 255 | where we assume `countWords` is a function that receives a `String` and returns a `Number`. 256 | 257 | In contrast, **chain**ing *Promises* and *Tasks* are useful when you need the result of a previous async operation in order to make the next one. For example the first request may return a JSON object with an url to make the next request. 258 | 259 | ```javascript 260 | fetch('http://some-rest.api') 261 | .then(obj => analyzeResponseAndGetTheUrl(obj)) 262 | .then(url => fetch(url)); 263 | .then(obj => alert(`Some data ${obj.foo}`)); 264 | 265 | fetchTask('http://some-rest.api') 266 | .map(obj => analyzeResponseAndGetTheUrl(obj)) 267 | .chain(url => fetchTask(url)); 268 | .fork( 269 | noop, 270 | obj => alert(`Some data ${obj.foo}`) 271 | ); 272 | 273 | ``` 274 | 275 | In this example we are synchronously **transform**ing the first response using the `analyzeResponseAndGetTheUrl` function that receives an `Object` and returns a `String` and then **chain**ing the eventual transformation with the second call to `fetch`. 276 | 277 | This should give you an intuition that whenever you see a `map` you are not adding more time to your computation, but when you are using `chain` you are most likely are. 278 | 279 | 280 | ## Credits 281 | 282 | Initialized with [@alexjoverm](https://twitter.com/alexjoverm)'s [TypeScript Library Starter](https://github.com/alexjoverm/typescript-library-starter) under the Acamica Labs initiative and made with :heart: by 283 | 284 | 285 | 286 | | [
Hernan Rajchert](https://github.com/hrajchert)
[💻](https://github.com/hrajchert/@ts-task/task/commits?author=hrajchert "Code") [🎨](#design-hrajchert "Design") [📖](https://github.com/hrajchert/@ts-task/task/commits?author=hrajchert "Documentation") [💡](#example-hrajchert "Examples") [⚠️](https://github.com/hrajchert/@ts-task/task/commits?author=hrajchert "Tests") | [
Gonzalo Gluzman](https://github.com/dggluz)
[💻](https://github.com/hrajchert/@ts-task/task/commits?author=dggluz "Code") [🤔](#ideas-dggluz "Ideas, Planning, & Feedback") [⚠️](https://github.com/hrajchert/@ts-task/task/commits?author=dggluz "Tests") | 287 | | :---: | :---: | 288 | 289 | 290 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! 291 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at alexjovermorales@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ts-task/task", 3 | "version": "0.0.0-development", 4 | "description": "Promise replacement made with TypeScript more suitable for functional programming and error handling", 5 | "keywords": [ 6 | "async", 7 | "promise", 8 | "functional programming", 9 | "typescript" 10 | ], 11 | "main": "dist/task.umd.js", 12 | "module": "dist/task.es2015.js", 13 | "typings": "dist/lib/src/index.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "author": "Hernan Rajchert ", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/ts-task/task.git" 21 | }, 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">=6.0.0" 25 | }, 26 | "scripts": { 27 | "lint": "tslint -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", 28 | "prebuild": "rm -Rf dist/docs", 29 | "build": "tsls build --libraryName=index && typedoc --out dist/docs --target es6 --theme minimal src", 30 | "start": "tsc -w & rollup -c node_modules/tsls/default-config/rollup.config.ts -w", 31 | "test": "jest", 32 | "test:watch": "jest --watch", 33 | "test:prod": "npm run lint && npm run test -- --coverage --no-cache", 34 | "test:types": "dtslint test/types/", 35 | "deploy-docs": "ts-node tools/gh-pages-publish", 36 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 37 | "commit": "git-cz", 38 | "semantic-release": "semantic-release", 39 | "semantic-release-prepare": "ts-node tools/semantic-release-prepare", 40 | "precommit": "lint-staged", 41 | "contributors-add": "all-contributors add", 42 | "contributors-generate": "all-contributors generate" 43 | }, 44 | "lint-staged": { 45 | "{src,test}/**/*.ts": [ 46 | "git add" 47 | ] 48 | }, 49 | "config": { 50 | "commitizen": { 51 | "path": "node_modules/cz-conventional-changelog" 52 | }, 53 | "validate-commit-msg": { 54 | "types": "conventional-commit-types", 55 | "helpMessage": "Use \"npm run commit\" instead, we use conventional-changelog format :) (https://github.com/commitizen/cz-cli)" 56 | } 57 | }, 58 | "jest": { 59 | "transform": { 60 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 61 | }, 62 | "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts)$", 63 | "moduleFileExtensions": [ 64 | "ts", 65 | "tsx", 66 | "js" 67 | ], 68 | "coveragePathIgnorePatterns": [ 69 | "/node_modules/", 70 | "/test/", 71 | "/dist/" 72 | ], 73 | "coverageThreshold": { 74 | "global": { 75 | "branches": 90, 76 | "functions": 95, 77 | "lines": 95, 78 | "statements": 95 79 | } 80 | }, 81 | "collectCoverage": true 82 | }, 83 | "devDependencies": { 84 | "@types/jest": "^21.1.10", 85 | "@types/node": "^8.10.45", 86 | "all-contributors-cli": "^4.11.2", 87 | "colors": "^1.3.3", 88 | "commitizen": "^3.0.7", 89 | "coveralls": "^3.0.3", 90 | "cross-env": "^5.2.0", 91 | "cz-conventional-changelog": "^2.1.0", 92 | "dtslint": "^0.3.0", 93 | "husky": "^0.14.3", 94 | "jest": "^24.5.0", 95 | "lint-staged": "^6.1.1", 96 | "lodash.camelcase": "^4.3.0", 97 | "prompt": "^1.0.0", 98 | "replace-in-file": "^3.4.4", 99 | "rimraf": "^2.6.3", 100 | "semantic-release": "^15.13.3", 101 | "ts-jest": "^24.0.0", 102 | "ts-node": "^3.3.0", 103 | "tslint": "^5.14.0", 104 | "tslint-config-acamica": "^2.0.0", 105 | "tsls": "0.0.3", 106 | "typedoc": "^0.14.2", 107 | "typescript": "^3.0.1", 108 | "validate-commit-msg": "^2.14.0" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as operators from './operators'; 2 | export * from './task'; 3 | export { operators }; 4 | -------------------------------------------------------------------------------- /src/operators/as-unknown-error.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever } from '../../test/jest-helper'; 2 | import { Task, UnknownError } from '../task'; 3 | import { asUnknownError } from './as-unknown-error'; 4 | 5 | describe('asUnknownError', () => { 6 | it('Should catch any error', (cb) => { 7 | // GIVEN: A task created from a rejected promise (with any error type) 8 | const task = Task.fromPromise(Promise.reject('something')); 9 | 10 | // WHEN: We catch it as an UnknownError 11 | const result = task.catch(asUnknownError); 12 | 13 | // THEN: The Task is rejected with an UnknownError and the types get more specific 14 | // than any 15 | result.fork( 16 | assertFork(cb, err => expect(err).toBeInstanceOf(UnknownError)), 17 | jestAssertNever(cb) 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/operators/as-unknown-error.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '../task/task'; 2 | import { UnknownError } from '../task/unknown-error'; 3 | 4 | 5 | export function asUnknownError (error: any): Task { 6 | return Task.reject(new UnknownError(error)); 7 | } 8 | -------------------------------------------------------------------------------- /src/operators/catch-error.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from '../../test/jest-helper'; 2 | import { Task } from '../task'; 3 | import { UnknownError } from '../task/unknown-error'; 4 | 5 | describe('Task', () => { 6 | describe('catch', () => { 7 | it('Should not call the function when the task is not failed', (cb) => { 8 | // GIVEN: a resolved task 9 | const task = Task.resolve('0'); 10 | 11 | // WHEN: we catch the error, THEN: the function is never called 12 | const result = task.catch(x => { 13 | jestAssertNever(cb)(x); 14 | return Task.resolve(0); 15 | }); 16 | 17 | // ... and the success function should be called 18 | result.fork( 19 | jestAssertNever(cb), 20 | assertFork(cb, x => expect(x).toBe('0')) 21 | ); 22 | }); 23 | 24 | it('Should resolve to the catched value from a rejected task', (cb) => { 25 | // GIVEN: a rejected task 26 | const task = Task.reject('buu'); 27 | 28 | // WHEN: we catch the error 29 | const result = task.catch(x => Task.resolve(0)); 30 | 31 | // THEN: the success function should be called 32 | result.fork( 33 | jestAssertNever(cb), 34 | assertFork(cb, x => expect(x).toBe(0)) 35 | ); 36 | }); 37 | 38 | it('Should resolve to the catched value from a task that throws', (cb) => { 39 | // GIVEN: a resolved task with a mapped function that throws 40 | const task = Task 41 | .resolve('wii') 42 | .map(val => { 43 | const t = true; 44 | if (t === true) { 45 | throw new Error('buu'); 46 | } 47 | return val; 48 | }); 49 | 50 | // WHEN: we catch the error 51 | const result = task.catch(x => Task.resolve(0)); 52 | 53 | // THEN: the success function should be called 54 | result.fork( 55 | jestAssertNever(cb), 56 | assertFork(cb, x => expect(x).toBe(0)) 57 | ); 58 | }); 59 | 60 | it('Should transform the error', (cb) => { 61 | // GIVEN: a rejected task 62 | const task = Task.reject('buu'); 63 | 64 | // WHEN: we catch the error, returning a new error 65 | const result = task.catch(x => Task.reject(true)); 66 | 67 | // THEN: the error function should be called with the new error 68 | result.fork( 69 | assertFork(cb, err => expect(err).toBe(true)), 70 | jestAssertNever(cb) 71 | ); 72 | }); 73 | 74 | it('Should work with functions that throws', (cb) => { 75 | // GIVEN: a rejected task 76 | const task = Task.reject('buu'); 77 | 78 | // WHEN: we catch the error, returning a new error 79 | const result = task.catch(x => {throw new Error('buu'); }); 80 | 81 | // THEN: the error function should be called with the new error 82 | result.fork( 83 | assertFork(cb, x => {expect(x).toBeInstanceOf(UnknownError); }), 84 | jestAssertUntypedNeverCalled(cb) 85 | ); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/operators/catch-error.ts: -------------------------------------------------------------------------------- 1 | import { ITaskChainFn, Task } from '../task/task'; 2 | import { UnknownError } from '../task/unknown-error'; 3 | 4 | export function catchError (fn: ITaskChainFn) { 5 | return function (input: Task): Task { 6 | return new Task((outerResolve, outerReject) => { 7 | input.fork( 8 | err => { 9 | try { 10 | fn(err).fork(outerReject, outerResolve); 11 | } 12 | catch (err) { 13 | outerReject(new UnknownError(err)); 14 | } 15 | }, 16 | outerResolve 17 | ); 18 | }); 19 | }; 20 | } -------------------------------------------------------------------------------- /src/operators/chain.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from '../../test/jest-helper'; 2 | import { Task } from '../task'; 3 | import { UnknownError } from '../task/unknown-error'; 4 | 5 | describe('Task', () => { 6 | describe('chain', () => { 7 | it('Should handle functions that succeed', (cb) => { 8 | // GIVEN: a resolved value 9 | const resolved = Task.resolve(1); 10 | 11 | // WHEN: we chain it with another task that succeed 12 | const result = resolved.chain(n => Task.resolve(n * 2)); 13 | 14 | // THEN: the success handler should be called with the chained result 15 | result.fork( 16 | jestAssertNever(cb), 17 | assertFork(cb, x => expect(x).toBe(2)) 18 | ); 19 | }); 20 | 21 | it('Should handle functions that throw', (cb) => { 22 | // GIVEN: a resolved value 23 | const resolved = Task.resolve(0); 24 | 25 | // WHEN: we chain with a function that throws 26 | const result = resolved.chain(n => {throw new Error('buu'); }); 27 | 28 | // THEN: the error handler should be called 29 | result.fork( 30 | assertFork(cb, x => {expect(x).toBeInstanceOf(UnknownError); }), 31 | jestAssertUntypedNeverCalled(cb) 32 | ); 33 | }); 34 | 35 | it('Should propagate error', (cb) => { 36 | // GIVEN: a rejected Task 37 | const rejected = Task.reject('buu'); 38 | 39 | // WHEN: we chain the function, THEN: the chained function is never called 40 | const result = rejected.chain(jestAssertNever(cb)); 41 | 42 | // ...and the error propagates 43 | result.fork( 44 | assertFork(cb, x => expect(x).toBe('buu')), 45 | jestAssertUntypedNeverCalled(cb) 46 | ); 47 | }); 48 | 49 | it('Should handle rejection in the chained function', (cb) => { 50 | // GIVEN: a resolved value 51 | const resolved = Task.resolve(1); 52 | 53 | // WHEN: we chained with a rejected value 54 | const result = resolved.chain(_ => Task.reject('buu')); 55 | 56 | // THEN: the error handler should be called with the rejected value 57 | result.fork( 58 | assertFork(cb, x => expect(x).toBe('buu')), 59 | jestAssertNever(cb) 60 | ); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/operators/chain.ts: -------------------------------------------------------------------------------- 1 | import { ITaskChainFn, Task } from '../task/task'; 2 | import { UnknownError } from '../task/unknown-error'; 3 | 4 | export function chain (fn: ITaskChainFn) { 5 | return function (input: Task): Task { 6 | return new Task((outerResolve, outerReject) => { 7 | input.fork( 8 | outerReject, 9 | value => { 10 | try { 11 | fn(value).fork(outerReject, outerResolve); 12 | } 13 | catch (err) { 14 | outerReject(new UnknownError(err)); 15 | } 16 | } 17 | ); 18 | }); 19 | }; 20 | } -------------------------------------------------------------------------------- /src/operators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './as-unknown-error'; 2 | export * from './catch-error'; 3 | export * from './chain'; 4 | export * from './map'; 5 | -------------------------------------------------------------------------------- /src/operators/map.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever } from '../../test/jest-helper'; 2 | import { map } from '../operators'; 3 | import { Task } from '../task'; 4 | import { UnknownError } from '../task/unknown-error'; 5 | 6 | describe('Task', () => { 7 | describe('map', () => { 8 | it('Should handle transformation functions (dot-chaining)', (cb) => { 9 | // GIVEN: a resolved value 10 | const task = Task.resolve(0); 11 | 12 | // WHEN: we transform the value 13 | const result = task.map(n => '' + n); 14 | 15 | // THEN: the success function should be called with the transformed value 16 | result.fork( 17 | jestAssertNever(cb), 18 | assertFork(cb, x => expect(x).toBe('0')) 19 | ); 20 | }); 21 | 22 | it('Should handle transformation functions (pipe)', (cb) => { 23 | // GIVEN: a resolved value 24 | const task = Task.resolve(0); 25 | 26 | // WHEN: we transform the value 27 | const result = task.pipe( 28 | map(n => '' + n), 29 | ); 30 | 31 | // THEN: the success function should be called with the transformed value 32 | result.fork( 33 | jestAssertNever(cb), 34 | assertFork(cb, x => expect(x).toBe('0')) 35 | ); 36 | }); 37 | 38 | it('Should handle functions that throw', (cb) => { 39 | // GIVEN: a resolved value 40 | const resolved = Task.resolve(0); 41 | 42 | // WHEN: we map with a transformation that throws 43 | const result = resolved.map(n => {throw new Error('buu'); }); 44 | 45 | // THEN: the error handler should be called 46 | result.fork( 47 | assertFork(cb, x => expect(x instanceof UnknownError).toBe(true)), 48 | jestAssertNever(cb) 49 | ); 50 | }); 51 | 52 | it('Should propagate error', (cb) => { 53 | // GIVEN: a rejected Task 54 | const rejected = Task.reject('buu'); 55 | 56 | // WHEN: we map the function, THEN: the mapped function is never called 57 | const result = rejected.map(jestAssertNever(cb)); 58 | 59 | // ...and the error propagates 60 | result.fork( 61 | assertFork(cb, x => expect(x).toBe('buu')), 62 | jestAssertNever(cb) 63 | ); 64 | }); 65 | 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/operators/map.ts: -------------------------------------------------------------------------------- 1 | import { IMapFn, Task } from '../task/task'; 2 | import { UnknownError } from '../task/unknown-error'; 3 | 4 | export function map (fn: IMapFn) { 5 | return function (input: Task): Task { 6 | return new Task((outerResolve, outerReject) => { 7 | input.fork( 8 | outerReject, 9 | value => { 10 | try { 11 | const result = fn(value); 12 | outerResolve(result); 13 | } catch (error) { 14 | outerReject(new UnknownError(error)); 15 | } 16 | } 17 | ); 18 | }); 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/task/index.ts: -------------------------------------------------------------------------------- 1 | export * from './task'; 2 | export * from './unknown-error'; 3 | 4 | import './task-all'; 5 | import './task-from-promise'; 6 | import './task-operators'; 7 | import './task-pipe'; 8 | -------------------------------------------------------------------------------- /src/task/task-all.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from '../../test/jest-helper'; 2 | import { Task } from './index'; 3 | 4 | describe('Task', () => { 5 | describe('all', () => { 6 | it('should work with a single resolved Task', cb => { 7 | // GIVEN: a resolved Task 8 | const task = Task.resolve(5); 9 | 10 | // WHEN: we do a Task.all from the previous one Task 11 | const tAll = Task.all([task]); 12 | 13 | // THEN: the resulting Task is resolved with an array of the resolved value 14 | tAll.fork( 15 | jestAssertNever(cb), 16 | assertFork(cb, x => expect(x).toEqual([5])) 17 | ); 18 | }); 19 | 20 | it('should mantain types when given an array of elements of the same type', cb => { 21 | // GIVEN: a resolved Task 22 | const task = Task.resolve(5); 23 | 24 | const multipleTasks = [task, task, task, task, task, task, task, task, task, task, task]; 25 | 26 | // WHEN: we do a Task.all from the previous tasks 27 | const tAll = Task.all(multipleTasks); 28 | 29 | 30 | // THEN: the resulting Task is resolved with an array of the resolved value 31 | tAll.fork( 32 | jestAssertNever(cb), 33 | assertFork(cb, x => expect(x).toEqual([5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5])) 34 | ); 35 | }); 36 | 37 | it('should work with a single rejected Task', cb => { 38 | // GIVEN: a rejected Task 39 | const task = Task.reject('Buu!'); 40 | 41 | // WHEN: we do a Task.all from the previous one Task 42 | const tAll = Task.all([task]); 43 | 44 | // THEN: the resulting Task is rejected with the rejected error 45 | tAll.fork( 46 | assertFork(cb, err => expect(err).toEqual('Buu!')), 47 | jestAssertUntypedNeverCalled(cb) 48 | ); 49 | }); 50 | 51 | it('should resolve if all task are resolved', cb => { 52 | // GIVEN: a bunch of resolved Tasks 53 | const task1 = Task.resolve(10); 54 | const task2 = Task.resolve('100'); 55 | const task3 = Task.resolve(true); 56 | 57 | // WHEN: we do a Task.all from the previous Tasks 58 | const tAll = Task.all([task1, task2, task3]); 59 | 60 | // THEN: the resulting Task is resolved with the resolved values 61 | tAll.fork( 62 | jestAssertNever(cb), 63 | assertFork(cb, x => expect(x).toEqual([10, '100', true])) 64 | ); 65 | }); 66 | 67 | it('should wait async tasks', cb => { 68 | // GIVEN: a bunch of resolved Tasks 69 | const task1 = Task.resolve(10); 70 | const task2 = new Task(resolve => setTimeout(_ => resolve('foo'), 10)); 71 | const task3 = Task.resolve(true); 72 | 73 | // WHEN: we do a Task.all from the previous Tasks 74 | const tAll = Task.all([task1, task2, task3]); 75 | 76 | // THEN: the resulting Task is resolved with the resolved values 77 | tAll.fork( 78 | jestAssertNever(cb), 79 | assertFork(cb, x => expect(x).toEqual([10, 'foo', true])) 80 | ); 81 | }); 82 | 83 | it('should reject if there is even one rejected one', cb => { 84 | // GIVEN: a rejected Task 85 | const task1 = Task.resolve(10); 86 | const task2 = Task.reject('Buu!'); 87 | const task3 = Task.resolve(1000); 88 | 89 | // WHEN: we do a Task.all from the previous one Task 90 | const tAll = Task.all([task1, task2, task3]); 91 | 92 | // THEN: the resulting Task is rejected with the rejected error 93 | tAll.fork( 94 | assertFork(cb, err => expect(err).toEqual('Buu!')), 95 | jestAssertUntypedNeverCalled(cb) 96 | ); 97 | }); 98 | 99 | it('should reject with the first rejection', cb => { 100 | // GIVEN: three rejected Task 101 | const task1 = Task.reject('Foo'); 102 | const task2 = new Task((_, reject) => setTimeout(_ => reject(9), 0)); 103 | const task3 = new Task((_, reject) => setTimeout(_ => reject(true), 0)); 104 | 105 | // WHEN: we do a Task.all from the previous one Task 106 | const tAll = Task.all([task1, task2, task3]); 107 | 108 | // THEN: the resulting Task is rejected with the first rejected error 109 | tAll.fork( 110 | err => { 111 | expect(err).toEqual('Foo'); 112 | // Need to wait to make sure we test all lines in coverage 113 | setTimeout(cb, 10); 114 | }, 115 | jestAssertUntypedNeverCalled(cb) 116 | ); 117 | }); 118 | 119 | it('should resolve to an empty array if it\'s called with an empty array', cb => { 120 | // GIVEN: an empty array 121 | const arr: Task[] = []; 122 | 123 | // WHEN: calling `Task.all` with that array 124 | const tAll = Task.all(arr); 125 | 126 | // THEN: the resulted Task is resolved with an empty array. 127 | tAll.fork( 128 | jestAssertNever(cb), 129 | assertFork(cb, result => { 130 | expect(result).toEqual([]); 131 | }) 132 | ); 133 | }); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /src/task/task-all.ts: -------------------------------------------------------------------------------- 1 | import { Task } from './task'; 2 | 3 | declare module './task' { 4 | // tslint:disable-next-line:interface-name 5 | namespace Task { 6 | function all (tasks: [Task, Task, Task, Task, Task, Task, Task, Task, Task, Task]): Task<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9 | E10>; 7 | function all (tasks: [Task, Task, Task, Task, Task, Task, Task, Task, Task]): Task<[T1, T2, T3, T4, T5, T6, T7, T8, T9], E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 | E9>; 8 | function all (tasks: [Task, Task, Task, Task, Task, Task, Task, Task]): Task<[T1, T2, T3, T4, T5, T6, T7, T8], E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8>; 9 | function all (tasks: [Task, Task, Task, Task, Task, Task, Task]): Task<[T1, T2, T3, T4, T5, T6, T7], E1 | E2 | E3 | E4 | E5 | E6 | E7>; 10 | function all (tasks: [Task, Task, Task, Task, Task, Task]): Task<[T1, T2, T3, T4, T5, T6], E1 | E2 | E3 | E4 | E5 | E6>; 11 | function all (tasks: [Task, Task, Task, Task, Task]): Task<[T1, T2, T3, T4, T5], E1 | E2 | E3 | E4 | E5>; 12 | function all (tasks: [Task, Task, Task, Task]): Task<[T1, T2, T3, T4], E1 | E2 | E3 | E4>; 13 | function all (tasks: [Task, Task, Task]): Task<[T1, T2, T3], E1 | E2 | E3>; 14 | function all (tasks: [Task, Task]): Task<[T1, T2], E1 | E2>; 15 | function all (tasks: [Task]): Task<[T1], E1>; 16 | function all (tasks: Array>): Task; 17 | } 18 | } 19 | 20 | Task.all = function (tasks: any): any { 21 | // Flag to track if any Task has resolved 22 | let rejected = false; 23 | // Array that we'll fill with the resolved values, in order 24 | const resolvedValues: any[] = []; 25 | // Counter of resolved Tasks (we can't use resolvedValues.length since we add elements through index) 26 | let resolvedQty = 0; 27 | 28 | return new Task((outerResolve, outerReject) => { 29 | // If the tasks array is empty, resolve with an empty array. 30 | if (!tasks.length) { 31 | outerResolve([]); 32 | } 33 | 34 | tasks.forEach((aTask: any, index: number) => { 35 | aTask 36 | .fork((err: any) => { 37 | // We do only reject if there was no previous rejection 38 | if (!rejected) { 39 | rejected = true; 40 | outerReject(err); 41 | } 42 | }, (x: any) => { 43 | // Shouldn't resolve if another Task has rejected 44 | if (rejected) { 45 | return; 46 | } 47 | 48 | // Track resolved value (in order) 49 | resolvedValues[index] = x; 50 | // ...and how many tasks has resolved 51 | resolvedQty++; 52 | if (resolvedQty === tasks.length) { 53 | outerResolve(resolvedValues); 54 | } 55 | }); 56 | }); 57 | }); 58 | }; -------------------------------------------------------------------------------- /src/task/task-from-promise.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from '../../test/jest-helper'; 2 | import { Task } from './index'; 3 | 4 | describe('Task', () => { 5 | describe('fromPromise', () => { 6 | it('Should work with resolved promises', (cb) => { 7 | // GIVEN: A task created from a resolved promise 8 | const resolved = Task.fromPromise(Promise.resolve(0)); 9 | 10 | // WHEN: we fork, THEN: it should call the success function with the resolved value 11 | resolved.fork( 12 | jestAssertNever(cb), 13 | assertFork(cb, x => expect(x).toBe(0)), 14 | ); 15 | 16 | }); 17 | 18 | it('Should work with rejected promises', (cb) => { 19 | // GIVEN: A task created from a rejected promise 20 | const resolved = Task.fromPromise(Promise.reject('buu')); 21 | 22 | // WHEN: we fork, THEN: it should call the error function with the rejected value 23 | resolved.fork( 24 | assertFork(cb, x => expect(x).toBe('buu')), 25 | jestAssertNever(cb) 26 | ); 27 | }); 28 | 29 | it('Should mantain any error when changed', (cb) => { 30 | // GIVEN: A task created from a rejected promise 31 | const task = Task.fromPromise(Promise.reject('buu')); 32 | 33 | // WHEN: we chain it with some value 34 | const result = task.chain(_ => Task.resolve(0)); 35 | // THEN: the task type should be Task (check manually?) 36 | 37 | // and the error should be the same 38 | result.fork( 39 | assertFork(cb, x => expect(x).toBe('buu')), 40 | jestAssertUntypedNeverCalled(cb) 41 | ); 42 | }); 43 | 44 | it('Should be able to set rejected type', (cb) => { 45 | // GIVEN: A task created from a rejected promise 46 | const resolved = Task.fromPromise(Promise.reject('buu')); 47 | 48 | // WHEN: we fork, THEN: it should call the error function with the rejected value 49 | resolved.fork( 50 | assertFork(cb, x => expect(x).toBe('buu')), 51 | jestAssertNever(cb) 52 | ); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/task/task-from-promise.ts: -------------------------------------------------------------------------------- 1 | import { Task } from './task'; 2 | 3 | declare module './task' { 4 | // tslint:disable-next-line:interface-name 5 | namespace Task { 6 | function fromPromise (promise: Promise): Task; 7 | } 8 | } 9 | Task.fromPromise = function (promise: Promise): Task { 10 | return new Task((outerResolve, outerReject) => { 11 | promise.then(outerResolve, err => outerReject(err)); 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/task/task-operators.ts: -------------------------------------------------------------------------------- 1 | import { UnknownError } from '.'; 2 | import { catchError, chain, map } from '../operators'; 3 | import { IMapFn, ITaskChainFn, Task } from './task'; 4 | 5 | // This is a helper file to include the basic operators inside Task 6 | // and avoid a cyclic dependency. 7 | declare module './task' { 8 | // tslint:disable-next-line:interface-name 9 | interface Task { 10 | map (fn: IMapFn): Task; 11 | chain (fn: ITaskChainFn): Task < TResult, E | EResult | UnknownError >; 12 | catch (fn: ITaskChainFn): Task; 13 | } 14 | } 15 | 16 | Task.prototype.map = function 17 | (fn: IMapFn) { 18 | return map(fn)(this); 19 | }; 20 | 21 | Task.prototype.chain = function 22 | (fn: ITaskChainFn) { 23 | return chain(fn)(this); 24 | }; 25 | 26 | Task.prototype.catch = function 27 | (fn: ITaskChainFn) { 28 | return catchError(fn)(this); 29 | }; 30 | -------------------------------------------------------------------------------- /src/task/task-pipe.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from '../../test/jest-helper'; 2 | import { catchError, chain, map } from '../operators'; 3 | import { Task } from './index'; 4 | import { UnknownError } from './unknown-error'; 5 | 6 | describe('Task', () => { 7 | describe('pipe', () => { 8 | it('Should return the same task if no function is passed', (cb) => { 9 | // GIVEN: a resolved value 10 | const task = Task.resolve(0); 11 | 12 | // WHEN: we call pipe with no transformation 13 | const result = task.pipe(); 14 | 15 | // THEN: the success function should be called without transformation 16 | result.fork( 17 | jestAssertNever(cb), 18 | assertFork(cb, x => expect(x).toBe(0)) 19 | ); 20 | }); 21 | 22 | it('Should work with one function', (cb) => { 23 | // GIVEN: a resolved value 24 | const task = Task.resolve(0); 25 | 26 | // WHEN: we pipe the value 27 | const result = task.pipe( 28 | map(n => '' + n) 29 | ); 30 | 31 | // THEN: the success function should be called with the transformed value 32 | result.fork( 33 | jestAssertNever(cb), 34 | assertFork(cb, x => expect(x).toBe('0')) 35 | ); 36 | }); 37 | 38 | it('Should work with multiple functions', (cb) => { 39 | // GIVEN: a resolved value 40 | const task = Task.resolve(0); 41 | 42 | // WHEN: we pipe the value 43 | const result = task.pipe( 44 | map(n => '' + n), 45 | map(s => s + '!'), 46 | map(s => s + '!'), 47 | map(s => s + '!') 48 | ); 49 | 50 | // THEN: the success function should be called with the transformed value 51 | result.fork( 52 | jestAssertNever(cb), 53 | assertFork(cb, x => expect(x).toBe('0!!!')) 54 | ); 55 | }); 56 | 57 | it('Should work with multiple functions converting success types correctly', (cb) => { 58 | // GIVEN: a resolved value 59 | const task = Task.resolve(0); 60 | 61 | // WHEN: we pipe the value 62 | const result = task.pipe( 63 | map(n => '' + n), 64 | map(s => parseInt(s)), 65 | map(n => '' + n), 66 | map(s => parseInt(s)) 67 | ); 68 | 69 | // THEN: the success function should be called with the transformed value 70 | result.fork( 71 | jestAssertNever(cb), 72 | assertFork(cb, x => expect(x).toBe(0)) 73 | ); 74 | }); 75 | it('Should be lazy (dont call if not forked)', (cb) => { 76 | // GIVEN: a resolved value 77 | const task = Task.resolve(0); 78 | 79 | // and a manually created pipeable function 80 | const dontCall = (t: Task) => { 81 | // This should not be called 82 | expect(true).toBe(false); 83 | return t; 84 | }; 85 | 86 | // WHEN: we pipe the value but dont fork 87 | const result = task.pipe( 88 | dontCall 89 | ); 90 | 91 | // THEN: the content of the task is never called 92 | setTimeout(cb, 20); 93 | }); 94 | 95 | it('Should handle pipeable methods that throw', (cb) => { 96 | // GIVEN: a resolved value 97 | const task = Task.resolve(0); 98 | 99 | // and a manually created pipeable function 100 | const pipeThatThrows = (t: Task): Task => { 101 | throw 'oops'; 102 | }; 103 | 104 | // WHEN: we pipe the value 105 | const result = task.pipe( 106 | pipeThatThrows 107 | ); 108 | 109 | // THEN: the error function should be called with the new error 110 | result.fork( 111 | assertFork(cb, x => {expect(x).toBeInstanceOf(UnknownError); }), 112 | jestAssertUntypedNeverCalled(cb) 113 | ); 114 | }); 115 | 116 | it('Should be able to recover from pipeable methods that throw', (cb) => { 117 | // GIVEN: a resolved value 118 | const task = Task.resolve(0); 119 | const task2 = Task.resolve('0'); 120 | // and a manually created pipeable function 121 | const pipeThatThrows = (t: Task): Task => { 122 | throw 'oops'; 123 | }; 124 | 125 | // WHEN: we pipe the value 126 | const result = task.pipe( 127 | pipeThatThrows, 128 | catchError(err => task2) 129 | ); 130 | 131 | // THEN: the success function should be called with the catched value 132 | result.fork( 133 | jestAssertNever(cb), 134 | assertFork(cb, x => expect(x).toBe('0')) 135 | ); 136 | }); 137 | 138 | it('Should be able to propagate errors correctly', (cb) => { 139 | // GIVEN: a resolved value 140 | const task = Task.resolve(0); 141 | const rej = Task.reject('buu'); 142 | const task2 = Task.resolve('0'); 143 | 144 | // WHEN: we pipe the value 145 | const result = task.pipe( 146 | chain(val => rej), 147 | // err should be of type `string | UncauchtError` 148 | // The first because of rej, the second one because of chain 149 | catchError(err => task2) 150 | ); 151 | 152 | // THEN: the success function should be called with the catched value 153 | result.fork( 154 | jestAssertNever(cb), 155 | assertFork(cb, x => expect(x).toBe('0')) 156 | ); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /src/task/task-pipe.ts: -------------------------------------------------------------------------------- 1 | import { Task } from './task'; 2 | import { UnknownError } from './unknown-error'; 3 | 4 | declare module './task' { 5 | // tslint:disable-next-line:interface-name 6 | interface Task { 7 | pipe (): Task; 8 | pipe (f1: IPipeFn): Task; 9 | pipe (f1: IPipeFn, f2: IPipeFn): Task; 10 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn): Task; 11 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn): Task; 12 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn): Task; 13 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn): Task; 14 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn, f7: IPipeFn): Task; 15 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn, f7: IPipeFn, f8: IPipeFn): Task; 16 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn, f7: IPipeFn, f8: IPipeFn, f9: IPipeFn): Task; 17 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn, f7: IPipeFn, f8: IPipeFn, f9: IPipeFn, f10: IPipeFn): Task; 18 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn, f7: IPipeFn, f8: IPipeFn, f9: IPipeFn, f10: IPipeFn, f11: IPipeFn): Task; 19 | pipe (f1: IPipeFn, f2: IPipeFn, f3: IPipeFn, f4: IPipeFn, f5: IPipeFn, f6: IPipeFn, f7: IPipeFn, f8: IPipeFn, f9: IPipeFn, f10: IPipeFn, f11: IPipeFn, f12: IPipeFn): Task; 20 | } 21 | } 22 | 23 | Task.prototype.pipe = function (...fns: any[]): Task { 24 | return new Task((outerResolve, outerReject) => { 25 | const newTask = fns.reduce( 26 | (task, f) => { 27 | try { 28 | return f(task); 29 | } catch (err) { 30 | return Task.reject(new UnknownError(err)); 31 | } 32 | }, 33 | this 34 | ); 35 | return newTask.fork(outerReject, outerResolve); 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/task/task.test.ts: -------------------------------------------------------------------------------- 1 | import { assertFork, jestAssertNever, jestAssertUntypedNeverCalled } from '../../test/jest-helper'; 2 | import { Task } from './index'; 3 | 4 | describe('Task', () => { 5 | describe('fork', () => { 6 | it('should be lazy (dont call if not forked)', cb => { 7 | // GIVEN: a manually created task 8 | const task = new Task(resolve => { 9 | // This should not be called 10 | expect(true).toBe(false); 11 | }); 12 | 13 | // WHEN: we dont fork 14 | // THEN: the content of the task is never called 15 | setTimeout(cb, 20); 16 | }); 17 | 18 | it('should be lazy (called when forked)', cb => { 19 | // GIVEN: a manually created task 20 | const task = new Task(resolve => { 21 | // This should be called 22 | expect(true).toBe(true); 23 | cb(); 24 | }); 25 | 26 | // WHEN: we fork 27 | // THEN: the content of the task is called 28 | task.fork(x => x, x => x); 29 | }); 30 | 31 | it('Should be asynchronous', cb => { 32 | // GIVEN: A task that resolves in the future 33 | const task = new Task(resolve => 34 | setTimeout(_ => resolve('wii'), 10) 35 | ); 36 | 37 | // WHEN: we fork, THEN: it should call the success function in ten ms 38 | task.fork( 39 | jestAssertNever(cb), 40 | assertFork(cb, x => expect(x).toBe('wii')) 41 | ); 42 | }); 43 | 44 | it('Should call success handler on success', cb => { 45 | // GIVEN: A resolved task 46 | const task = Task.resolve(0); 47 | 48 | // WHEN: we fork 49 | // THEN: should call its success function with the resolved value 50 | task.fork( 51 | jestAssertNever(cb), 52 | assertFork(cb, x => expect(x).toBe(0)) 53 | ); 54 | }); 55 | 56 | it('Should call error handler on rejection', cb => { 57 | // GIVEN: A rejected task 58 | const task = Task.reject('buu'); 59 | 60 | // WHEN: we fork 61 | // THEN: should call the error handler with the error when it fails 62 | task.fork( 63 | assertFork(cb, x => expect(x).toBe('buu')), 64 | jestAssertNever(cb) 65 | ); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/task/task.ts: -------------------------------------------------------------------------------- 1 | import { UnknownError } from './unknown-error'; 2 | 3 | export type IMapFn = (a: A) => B; 4 | export type ITaskChainFn = (value: A) => Task; 5 | export type IPipeFn = (a: Task) => Task; 6 | 7 | /** 8 | * Asynchronous Task, like a promise but lazy and typed on error 9 | * @param T Type of the Task value on success 10 | * @param E Type of the Task error on failure 11 | */ 12 | export class Task { 13 | /** 14 | * Creates a new task using a `resolver` function. Pretty much like a promise creation 15 | * 16 | * ``` 17 | * const task = new Task((resolve, reject) => { 18 | * setTimeout(_ => resolve('value'), 1000) 19 | * }) 20 | * ``` 21 | * 22 | * @param resolver Function to resolve or reject the task 23 | * 24 | */ 25 | constructor (private resolver: (resolve: (value: T) => void, reject: (err: E) => void) => void) { 26 | 27 | } 28 | 29 | static resolve (value: T): Task { 30 | return new Task(resolve => { 31 | resolve(value); 32 | }); 33 | } 34 | 35 | // TODO: I would like to type as 36 | // but typescript infers wrongly 37 | static reject (error: E): Task { 38 | return new Task((resolve, reject) => { 39 | reject(error); 40 | }); 41 | } 42 | 43 | fork (errorFn: (error: E) => any, successFn: (value: T) => any): void { 44 | new Promise((resolve, reject) => { 45 | this.resolver(resolve, reject); 46 | }).then( 47 | (x: any) => successFn(x), 48 | errorFn 49 | ); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/task/unknown-error.test.ts: -------------------------------------------------------------------------------- 1 | import { UnknownError } from './unknown-error'; 2 | 3 | const functionThatThrowsAnError = () => { 4 | throw Error('Boo!'); 5 | }; 6 | 7 | const functionThatThrowsAString = () => { 8 | throw 'Oh No!'; 9 | }; 10 | 11 | const wrapInUnknown = (error: any) => { 12 | return new UnknownError(error); 13 | }; 14 | 15 | describe('UnknownError', () => { 16 | it('When created with an Error, UnknownError preserves the StackTrace', () => { 17 | try { 18 | // GIVEN: a function that throws an Error instance 19 | functionThatThrowsAnError(); 20 | } 21 | catch (err) { 22 | // WHEN: we wrap it in an UnknownError 23 | const wrappedInUnknownError = wrapInUnknown(err); 24 | 25 | // THEN: We have the stack for the creation of UnknownError concatenated with the original error 26 | expect(wrappedInUnknownError.stack).toContain('wrapInUnknown'); 27 | expect(wrappedInUnknownError.stack).toContain('-----'); 28 | expect(wrappedInUnknownError.stack).toContain('functionThatThrowsAnError'); 29 | } 30 | }); 31 | 32 | it('When created with a String, UnknownError has only its own stackTrace', () => { 33 | try { 34 | // GIVEN: a function that throws a string 35 | functionThatThrowsAString(); 36 | } 37 | catch (err) { 38 | // WHEN: we wrap it in an UnknownError 39 | const wrappedInUnknownError = wrapInUnknown(err); 40 | 41 | // THEN: We only have the stack for the creation of UnknownError 42 | expect(wrappedInUnknownError.stack).toContain('wrapInUnknown'); 43 | expect(wrappedInUnknownError.stack).not.toContain('-----'); 44 | expect(wrappedInUnknownError.stack).not.toContain('functionThatThrowsAString'); 45 | } 46 | }); 47 | 48 | it('UnknownError contains originalError', () => { 49 | try { 50 | // GIVEN: a function that throws an Error instance 51 | functionThatThrowsAnError(); 52 | } 53 | catch (err) { 54 | // WHEN: we wrap it in an UnknownError 55 | const wrappedInUnknownError = wrapInUnknown(err); 56 | 57 | // THEN: the original error is wrapped into the UnknownError. 58 | expect(wrappedInUnknownError.originalError).toBe(err); 59 | } 60 | }); 61 | 62 | it('When created with an Error, UnknownError\'s message contains the original error\'s message', () => { 63 | try { 64 | // GIVEN: a function that throws an Error instance 65 | functionThatThrowsAnError(); 66 | } 67 | catch (err) { 68 | // WHEN: we wrap it in an UnknownError 69 | const wrappedInUnknownError = wrapInUnknown(err); 70 | 71 | // THEN: the message will contain the words UnknownError and the original message 72 | expect(wrappedInUnknownError.message).toContain('UnknownError'); 73 | expect(wrappedInUnknownError.message).toContain(err.message); 74 | } 75 | }); 76 | 77 | it('When created with a String, UnknownError\'s message contains the original string', () => { 78 | try { 79 | // GIVEN: a function that throws a string 80 | functionThatThrowsAString(); 81 | } 82 | catch (err) { 83 | // WHEN: we wrap it in an UnknownError 84 | const wrappedInUnknownError = wrapInUnknown(err); 85 | 86 | // THEN: the message will contain the original string. 87 | expect(wrappedInUnknownError.message).toContain(err); 88 | } 89 | }); 90 | 91 | }); 92 | -------------------------------------------------------------------------------- /src/task/unknown-error.ts: -------------------------------------------------------------------------------- 1 | export class UnknownError extends Error { 2 | errorType = 'UnknownError' as 'UnknownError'; 3 | 4 | constructor (public originalError: any) { 5 | super(`UnknownError (${getErrorMessage(originalError)})`); 6 | 7 | Object.defineProperty(this, 'originalError', { 8 | enumerable: false, 9 | }); 10 | Object.defineProperty(this, 'errorType', { 11 | enumerable: false, 12 | }); 13 | 14 | if (isErrorInstance(originalError)) { 15 | this.stack = this.stack + '\n--------------\n' + originalError.stack; 16 | } 17 | } 18 | } 19 | 20 | function getErrorMessage (error: any): string { 21 | if (isErrorInstance(error)) { 22 | return error.message; 23 | } else { 24 | return error.toString(); 25 | } 26 | } 27 | 28 | function isErrorInstance (error: any): error is Error { 29 | return error instanceof Error; 30 | } -------------------------------------------------------------------------------- /test/jest-helper.ts: -------------------------------------------------------------------------------- 1 | import { UnknownError } from '../src/task'; 2 | 3 | export const jestAssertNever = 4 | (cb: jest.DoneCallback) => 5 | (obj: never | UnknownError) => 6 | cb('this should never happen', obj) 7 | ; 8 | 9 | export const jestAssertUntypedNeverCalled = 10 | (cb: jest.DoneCallback) => 11 | (obj: any) => 12 | cb('this should never happen', obj) 13 | ; 14 | 15 | export const assertFork = (cb: jest.DoneCallback, fn: (obj: T) => void) => (obj: T) => { 16 | try { 17 | fn(obj); 18 | cb(); 19 | } catch (err) { 20 | cb(err); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /test/types/basic-usage.ts: -------------------------------------------------------------------------------- 1 | import { Task, UnknownError } from '@ts-task/task'; 2 | // Task is parameterized in success (T) and error (E) 3 | 4 | // When you create one using Task.resolve or Task.reject TypeScript 5 | // will infer the type from the passed argument. 6 | 7 | Task.resolve(8); // $ExpectType Task 8 | Task.reject('Buu'); // $ExpectType Task 9 | 10 | // by default Task.resolve can't fail and Task.reject can't provide 11 | // a value so we use never to describe that. 12 | 13 | // Notice that if you create a Task using a Resolver the type can't 14 | // be infered 15 | const r1 = new Task((resolve, reject) => { 16 | resolve(1); 17 | }); 18 | r1; // $ExpectType Task<{}, {}> 19 | 20 | // So if you need to do it, make sure to pass the types manually 21 | const r2 = new Task((resolve, reject) => { 22 | resolve(1); 23 | }); 24 | r2; // $ExpectType Task 25 | 26 | // when we transform a Task, we can't be sure if the callback 27 | // we provide throws or not because at this moment TypeScript 28 | // doesn't have typed exceptions. 29 | // That's why we decided to catch all functions and return an 30 | // UnknownError if a function throws 31 | const t1 = Task 32 | .resolve(9) 33 | .map(x => x + 1); 34 | t1; // $ExpectType Task 35 | 36 | // If you want to add a specific error type, you can return a 37 | // rejected task in the chain operator 38 | const t2 = Task 39 | .resolve(9) 40 | .map(x => x + 1) 41 | .chain(x => 42 | x % 2 43 | ? Task.reject('I dont like pair numbers') 44 | : Task.resolve(x) 45 | ); 46 | t2; // $ExpectType Task 47 | 48 | // You may want to use something more manageable than a string, 49 | // that gives some context to the error and it's easier to distinguish 50 | 51 | // You can catch specific errors. 52 | // Notice that the error goes away from the types 53 | const t3 = Task 54 | .resolve(9) 55 | .catch(err => 56 | // Only handle string errors 57 | typeof err === 'string' 58 | ? Task.resolve(-1) 59 | : Task.reject(err) 60 | ); 61 | t3; // $ExpectType Task 62 | 63 | // Task forces you to check for errors, so when you fork the 64 | // first callback is the error and the second callback is the success. 65 | // You can always ignore it, but you'll have to do it consciously. 66 | Task.resolve(1).fork( 67 | err => void 0, // $ExpectType (err: never) => undefined 68 | val => void 0 // $ExpectType (val: number) => undefined 69 | ); 70 | -------------------------------------------------------------------------------- /test/types/index.d.ts: -------------------------------------------------------------------------------- 1 | // TypeScript Version: 2.4 2 | -------------------------------------------------------------------------------- /test/types/operators/all.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '@ts-task/task'; 2 | 3 | /************************************************ 4 | * Tasks with an unary tuple 5 | ***********************************************/ 6 | 7 | // Given a basic Task 8 | const t = Task.resolve(9); // $ExpectType Task 9 | 10 | // ...Task.all called with an unary tuple of that Task will give us a Task of an 11 | // unary tuple with the result 12 | Task.all([t]); // $ExpectType Task<[number], never> 13 | 14 | /************************************************ 15 | * Tasks with a n-ary tuple 16 | ***********************************************/ 17 | 18 | // Given three basic tasks 19 | const t1 = Task.resolve(9); // $ExpectType Task 20 | const t2 = Task.resolve('foo'); // $ExpectType Task 21 | const t3 = Task.resolve(true); // $ExpectType Task 22 | 23 | // ...Task.all will infer the success type T as a tuple of its arguments' success values, 24 | // and the error type E as any of the individual errors 25 | Task.all([t1, t2, t3]); // $ExpectType Task<[number, string, boolean], string | Error> 26 | 27 | // TODO: Document this error. 28 | // const allT = [t1, t2, t3]; 29 | 30 | // Task.all(allT); 31 | 32 | /************************************************ 33 | * Tasks with an array (not tuple) 34 | ***********************************************/ 35 | 36 | // Given an array of tasks: 37 | const arrOfTasks = [1, 2, 3] // $ExpectType Task[] 38 | .map(x => 39 | Task.resolve(x) 40 | ) 41 | ; 42 | 43 | // ...Task.all called with that array will give as a Task of an array: 44 | Task.all(arrOfTasks); // $ExpectType Task 45 | -------------------------------------------------------------------------------- /test/types/operators/from-promise.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '@ts-task/task'; 2 | 3 | // By default fromPromise can infer the value T from the promise, and the error 4 | // is anything (that's why we are using this library, to have typed errors) 5 | Task.fromPromise(Promise.resolve(9)); // $ExpectType Task 6 | 7 | // If you like, you can always specify the error type if you know it 8 | Task.fromPromise(Promise.resolve(9)); // $ExpectType Task -------------------------------------------------------------------------------- /test/types/operators/pipe.ts: -------------------------------------------------------------------------------- 1 | import { Task, operators } from '@ts-task/task'; 2 | 3 | const { map, chain } = operators; 4 | 5 | const t1 = Task.resolve(9); // $ExpectType Task 6 | 7 | const p1 = t1.pipe( 8 | map(x => '' + x) 9 | ); 10 | 11 | p1; // $ExpectType Task 12 | 13 | const p2 = t1.pipe( 14 | chain(_ => Task.reject('buu')) 15 | ); 16 | p2; // $ExpectType Task 17 | -------------------------------------------------------------------------------- /test/types/parametrize-reject-and-resolve.ts: -------------------------------------------------------------------------------- 1 | import { Task } from '@ts-task/task'; 2 | 3 | // Resolve and reject can infere one of the generics from the passed 4 | // argument, but the other one is defaulted to never and if necesary 5 | // it must be passed manually. 6 | 7 | // The problem normally arises when we find something like this 8 | function foo(condition: boolean) { 9 | if (condition) { 10 | return Task.resolve(9); 11 | } 12 | return Task.reject('buu'); 13 | } 14 | 15 | // This get's interpreted as 16 | foo; // $ExpectType (condition: boolean) => Task | Task 17 | // which it's probably not what we wanted. 18 | 19 | // The problem is that TypeScript can't do the OR distribution for us, we 20 | // need to provide the same type to each branch of the IF. 21 | 22 | // To make both Tasks match we need to specify the types when we create it 23 | Task.resolve(9); // $ExpectType Task 24 | Task.reject('buu'); // $ExpectType Task 25 | 26 | // Notice that in both cases we pass first the infered type and then the missing 27 | // one, so it can become a little verbose. If we did it the other way around 28 | // TypeScript could interpret things badly when only passing one type argument. 29 | // 30 | // Once Named Type Arguments lands in version 3.1 we could define it like this 31 | // 32 | // Task.resolve(9) 33 | // Task.reject('buu') 34 | // 35 | // but that would set the required TypeScript version too high 36 | // so will probably add it once it's been available for a while 37 | // PR: https://github.com/Microsoft/TypeScript/pull/23696 38 | 39 | // This would allow us to write our previous example like 40 | function foo2(condition: boolean) { 41 | if (condition) { 42 | return Task.resolve(9); 43 | } 44 | return Task.reject('buu'); 45 | } 46 | // And have the type as expected 47 | foo2; // $ExpectType (condition: boolean) => Task 48 | 49 | // This also happens if we are using the ternary operator 50 | const bar = (condition: boolean) => condition ? Task.resolve(9) : Task.reject('buu'); 51 | bar; // $ExpectType (condition: boolean) => Task | Task 52 | 53 | const bar2 = (condition: boolean) => 54 | condition 55 | ? Task.resolve(9) 56 | : Task.reject('buu'); 57 | 58 | bar2; // $ExpectType (condition: boolean) => Task 59 | -------------------------------------------------------------------------------- /test/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "outDir": "dist/lib", 5 | "moduleResolution": "node", 6 | "target": "es2015", 7 | "module":"es2015", 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "strictFunctionTypes": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "lib": ["es2015", "es2016", "es2017", "dom"], 14 | "sourceMap": true, 15 | "declaration": true, 16 | "allowSyntheticDefaultImports": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "baseUrl": ".", 20 | "paths": { "@ts-task/task": ["../../src"] } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/types/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dtslint/dtslint.json", 3 | "rules": { 4 | "no-useless-files": false, 5 | "file-name-casing": false, 6 | "max-line-length": false, 7 | "strict-export-declare-modifiers": false, 8 | /* I dont agree with the errors being reported. 9 | I think the main reason is because its intended to .d.ts 10 | */ 11 | "no-unnecessary-generics": false, 12 | /* I find this rule annoying, so I'm disabeling it ;) */ 13 | "eofline": false, 14 | "only-arrow-functions": false 15 | } 16 | } -------------------------------------------------------------------------------- /tools/gh-pages-publish.ts: -------------------------------------------------------------------------------- 1 | const { cd, exec, echo, touch } = require("shelljs") 2 | const { readFileSync } = require("fs") 3 | const url = require("url") 4 | 5 | let repoUrl 6 | let pkg = JSON.parse(readFileSync("package.json") as any) 7 | if (typeof pkg.repository === "object") { 8 | if (!pkg.repository.hasOwnProperty("url")) { 9 | throw new Error("URL does not exist in repository section") 10 | } 11 | repoUrl = pkg.repository.url 12 | } else { 13 | repoUrl = pkg.repository 14 | } 15 | 16 | let parsedUrl = url.parse(repoUrl) 17 | let repository = (parsedUrl.host || "") + (parsedUrl.path || "") 18 | let ghToken = process.env.GH_TOKEN 19 | 20 | echo("Deploying docs!!!") 21 | cd("dist/docs") 22 | touch(".nojekyll") 23 | exec("git init") 24 | exec("git add .") 25 | exec('git config user.name "Hernan Rajchert"') 26 | exec('git config user.email "hrajchert@gmail.com"') 27 | exec('git commit -m "docs(docs): update gh-pages"') 28 | exec( 29 | `git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages` 30 | ) 31 | echo("Docs deployed!!") 32 | -------------------------------------------------------------------------------- /tools/semantic-release-prepare.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { fork } = require('child_process'); 3 | const colors = require('colors'); 4 | 5 | const { readFileSync, writeFileSync } = require('fs'); 6 | const pkg = JSON.parse( 7 | readFileSync(path.resolve(__dirname, '..', 'package.json')) 8 | ); 9 | 10 | pkg.scripts.prepush = 'npm run test:prod && npm run build'; 11 | pkg.scripts.commitmsg = 'commitlint -E GIT_PARAMS'; 12 | 13 | writeFileSync( 14 | path.resolve(__dirname, '..', 'package.json'), 15 | JSON.stringify(pkg, null, 2) 16 | ); 17 | 18 | // Call husky to set up the hooks 19 | fork(path.resolve(__dirname, '..', 'node_modules', 'husky', 'bin', 'install')); 20 | 21 | console.log(); 22 | console.log(colors.green('Done!!')); 23 | console.log(); 24 | 25 | if (pkg.repository.url.trim()) { 26 | console.log(colors.cyan('Now run:')); 27 | console.log(colors.cyan(' npm install -g semantic-release-cli')); 28 | console.log(colors.cyan(' semantic-release-cli setup')); 29 | console.log(); 30 | console.log( 31 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 32 | ); 33 | console.log(); 34 | console.log( 35 | colors.gray( 36 | 'Note: Make sure "repository.url" in your package.json is correct before' 37 | ) 38 | ); 39 | } else { 40 | console.log( 41 | colors.red( 42 | 'First you need to set the "repository.url" property in package.json' 43 | ) 44 | ); 45 | console.log(colors.cyan('Then run:')); 46 | console.log(colors.cyan(' npm install -g semantic-release-cli')); 47 | console.log(colors.cyan(' semantic-release-cli setup')); 48 | console.log(); 49 | console.log( 50 | colors.cyan('Important! Answer NO to "Generate travis.yml" question') 51 | ); 52 | } 53 | 54 | console.log(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/tsls/default-config/tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "outDir": "dist/lib", 6 | "typeRoots": [ 7 | "node_modules/@types" 8 | ], 9 | "baseUrl": ".", 10 | "paths": { "@ts-task/task": ["./src"] } 11 | }, 12 | "include": [ 13 | "src", 14 | "test" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "interface-name": false, 4 | "only-arrow-functions": false 5 | }, 6 | "extends": [ 7 | "tslint-config-acamica" 8 | ] 9 | } 10 | --------------------------------------------------------------------------------