├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src ├── index.ts ├── maybe.spec.ts ├── maybe.ts ├── monad.spec.ts ├── monad.ts ├── result.spec.ts ├── result.ts ├── testUtils.ts ├── thenable.spec.ts └── thenable.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | .vscode/ 3 | coverage/ 4 | dist/ 5 | node_modules/ 6 | *.log 7 | .idea 8 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | .vscode/ 3 | coverage/ 4 | node_modules/ 5 | src/ 6 | .travis.yml 7 | *.log 8 | .idea 9 | package-lock.json 10 | tsconfig.json 11 | tslint.json 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | install: 10 | - npm install 11 | 12 | jobs: 13 | include: 14 | - stage: lint 15 | script: npm run lint 16 | - stage: build 17 | script: npm run build 18 | - stage: test 19 | script: npm run test:ci 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alexey Tukalo 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amonad 2 | [![Build Status](https://travis-ci.org/AIRTucha/amonad.svg?branch=master)](https://travis-ci.org/AIRTucha/amonad) [![Coverage Status](https://coveralls.io/repos/github/AIRTucha/amonad/badge.svg?branch=master)](https://coveralls.io/github/AIRTucha/amonad?branch=master) 3 | 4 | Implementation of *Maybe* and *Result* monads compatible with async/await. Learn more about monads [here](https://dev.to/airtucha/functors-and-monads-in-plain-typescript-33o1). 5 | 6 | *Maybe* is a container dedicated for the handling of a data which might be missing. Typically, it is used for representation of optional values. It allows prevent usage of Nullable objects. *Result* is an expansion of *Maybe* which can additionally carry the reason of unavailability. It is mainly utilized to represent the output of an operation which might fail since *Result* is also capable of containing an error message. 7 | 8 | Both of them implements a fraction of Promise's functionality. It allows us to model them via Promise. Therefore by the implementation of PromiseLike interface, they became compatible with async/await syntax. 9 | 10 | ## Get started 11 | 12 | The package is available via *npm*. It has to be installed as a local dependency: 13 | 14 | npm install amonad 15 | 16 | Each of them can be represented by values of two types: *Just* and *None* for *Maybe*, *Success* and *Failure* for *Result*. The values have dedicated factory functions with correspondent names. They can also be created by a *smart factories* which are just *Maybe* and *Result*. They correctly create the monads based on the provided argument. 17 | 18 | A primary way to access inclosed values is *then()* method. It expects two optional arguments which are represented by functions which perform operations over the internal state of the containers. The first one processes the data, and the second is used for handling of its absents. 19 | 20 | Also, there is an API for checking of an object type. It consist of *isMaybe*, *isJust*, *isNone*, *isResult*, *isSuccess* and *isFailure* functions which accept any object as an argument and returns result of the assertion as boolean value. Moreover, there are *isJust* and *isNone* methods for *Maybe*. Correspondingly, there are *isSuccess* and *isFailure* methods for *Result*. 21 | 22 | The carried information can be accessed via *get* and *getOrElse* methods. The first one returns the value inclosed inside of the container. The second one returns only the primary value from the monad and falls back to the provided argument if the primary value is not available. 23 | 24 | ## Usage 25 | 26 | Typical usage of *Maybe* and *Result* is very similar. Sometimes it is hardly possible to make a choice, but there is a clear semantic difference in the intention behind each of them. 27 | 28 | *Maybe*, primary, should represent values which might not be available by design. The most obvious example is return type of *Dictionary*: 29 | 30 | ```typescript 31 | interface Dictionary { 32 | set(key: K, value: V): void 33 | get(key: K): Maybe 34 | } 35 | ``` 36 | 37 | It can also be used as a representation of optional value. The following example shows the way to model a *User* interface with *Maybe*. Some nationalities have a second name as an essential part of their identity other not. Therefore the value can nicely be treated as *Maybe*. 38 | 39 | ```typescript 40 | interface Client { 41 | name: string 42 | secondName: Maybe 43 | lastName: string 44 | } 45 | ``` 46 | 47 | Computations which might fail due to obvious reason are also a good application for *Maybe*. Lowest common denominator might be unavailable. That is why the signature makes perfect sense for *getLCD()* function: 48 | 49 | ```typescript 50 | getLCD(num1: number, num2: number): Maybe 51 | ``` 52 | 53 | *Result* is mainly used for the representation of value which might unavailable for an uncertain reason or for tagging of a data which absents can significantly affect execution flow. For example, some piece of class’s state, required for computation, might be configured via an input provided during life-circle of the object. In this case, the default status of the property can be represented by *Failure* which would clarify, that computation is not possible until the state is not initialized. Following example demonstrates the described scenario. The method will return the result of the calculation as *Success* or “Data is not initialized” error message as Failure. 54 | 55 | ```typescript 56 | class ResultExample { 57 | value: Result = Failure(“Data is not initialized”) 58 | 59 | init( value: Value ) { 60 | this.value = Success(value) 61 | } 62 | 63 | calculateSomethingBasedOnValue(){ 64 | return this.value.then( value => 65 | someValueBasedComputation( value, otherArgs) 66 | ) 67 | } 68 | } 69 | ``` 70 | 71 | Moreover, monadic error handling is able to replace exceptions as the primary solution for error propagation. Following example presents a possible type signature for a parsing function which utilizes Result as a return type. 72 | 73 | ```typescript 74 | parseData( str: string ): Result 75 | ``` 76 | 77 | The output of such a function might contain processed value as *Success* or *Throwable* with an explanation of an error as *Failure*. 78 | 79 | ### Creation 80 | 81 | As I already said it is possible to instantiate *Maybe* and *Result* via factory functions. Different ways to create the monads is presented in the following code snippet. 82 | 83 | ```typescript 84 | const just = Just(3.14159265) 85 | const none = None() 86 | const success = Success("Iron Man") 87 | const failure: Failure = Failure( new Error("Does not exist.")) 88 | ``` 89 | 90 | NaN safe division function can be created using this library in the way demonstrated below. 91 | 92 | ```typescript 93 | const divide = (numerator: number, quotient: number ): Result => 94 | quotient !== 0 ? 95 | Success( numerator/quotient ) 96 | : 97 | Failure("It is not possible to divide by 0") 98 | ``` 99 | 100 | *Smart Maybe factory* is handy if there is Nullable object which has to be wrapped by *Maybe*. 101 | 102 | ```typescript 103 | const maybe = Maybe(map.get()) 104 | ``` 105 | 106 | *Smart Result factory* expects a function *() => T | Success | Failure* which might throw object of E type as exception. *Result* wraps around the output accordingly. 107 | 108 | ```typescript 109 | const data = Result( () => JSON.parse(inputStr) ) 110 | ``` 111 | 112 | ### Data handling 113 | 114 | The first argument of *then()* method is handler responsible for processing of expected value. It accepts two kinds of output values: value of arbitrary, *monad* of the same type. 115 | ```typescript 116 | // converts number value to string 117 | const eNumberStr: Maybe = Just(2.7182818284) 118 | .then( 119 | eNumber => `E number is: ${eNumber}` 120 | ) 121 | // checks if string is valid and turns the monad to None if not 122 | const validValue = Just(inputStr) 123 | .then( str => 124 | isValid(inputStr) ? 125 | str 126 | : 127 | None() 128 | ) 129 | ``` 130 | 131 | The content can also be access by *get()*, *getOrElse()* and *getOrThrow()* methods. *get()* output a union type of the value type and the error one for *Result* and the union type of the value and undefined for *Maybe*. The issue can be resolved by validation of the monad type by *isJust()* and *isSuccess()* methods or functions. 132 | 133 | ```typescript 134 | if(maybe.isJust()) { // it is also possible to write it via isJust(maybe) 135 | const value = maybe.get(); // return the value here 136 | // Some other actions... 137 | } else { 138 | // it does not make sense to call get() here since the output is going to be undefined 139 | // Some other actions... 140 | } 141 | 142 | if(result.isSuccess()) { // it is also possible to write it via isSuccess(result) 143 | const error = result.get(); // return the value here 144 | // Some other actions... 145 | } else { 146 | const value = result.get(); // return the error here 147 | // Some other actions... 148 | } 149 | ``` 150 | 151 | The main advantage of the library against other existing implementations of *Maybe/Option* and *Result/Try* monads for JavaScript is compatibility with *async/await* syntax. It is possible to *await* *Result* and *Maybe* inside of *async* functions. Anyway, the output is going to be wrapped by *Promise*. 152 | 153 | ```typescript 154 | const someStrangeMeaninglessComputations = async (num1: number, num2: number, num3: number ): Promise => { 155 | const lcd = await getLCD(num1, num2) // will throw undefined in case LCD does not exist 156 | return await divide(lcd, num3) 157 | } 158 | ``` 159 | 160 | ### Error handling 161 | 162 | The second argument of the *then()* method is a callback responsible for the handling of unexpected behavior. It works a bit differently for *Result* and *Maybe*. *None* has no value, that's why its callback doesn't have an argument. Additionally, it doesn't accept mapping to the value, since it should produce another *None* which also cannot contain any data. But returning of Just might be utilized to recovery *Maybe*. It is also possible to pass a void procedure to perform some side effect, for example, logging. *Failure* oriented handler works a bit more similar to the first one. It accepts two kinds of output values: the value of Throwable and *monad* of the *Result* type. 163 | 164 | ```typescript 165 | // tries to divide number e by n, recoveries to Infinity if division is not possible 166 | const eDividedByN: Failure = divide(2.7182818284, n) 167 | .then( 168 | eNumber => `E number divided by n is: ${eNumber}`, 169 | error => Success(Infinity) 170 | ) 171 | // looks up color from a dictionary by key, if color is not available falls back to black 172 | const valueFrom = colorDictionary.get(key) 173 | .then( 174 | undefined, 175 | () => Just("#000000") 176 | ) 177 | ``` 178 | 179 | It is also possible to verify if the monads are *Failure* or *None*, it can be done via *isNone()* and *isFailure()* methods and functions. 180 | 181 | ```typescript 182 | if(maybe.isNone()) { // it is also possible to write it via isNone(maybe) 183 | // it does not make sense to call get() here since the output is going to be undefined 184 | // Some other actions... 185 | } else { 186 | const value = maybe.get(); // return the value here 187 | // Some other actions... 188 | } 189 | 190 | if(result.isFailure()) { // it is also possible to write it via isFailure(result) 191 | const value = result.get(); // return the error here 192 | // Some other actions... 193 | } else { 194 | const error = result.get(); // return the value here 195 | // Some other actions... 196 | } 197 | ``` 198 | 199 | Awaiting of *None* and *Failure* led to throwing of an exception inside of async function. *None* doesn't have inclosed value. Therefore *undefined* is thrown. *Failure* conveniently store *Throwable* object which fulfils its purpose in such an occasion. Beyond *async* function the error is propagated as rejected *Promise*. 200 | 201 | ```typescript 202 | const someStrangeMeaninglessComputations = async (num1: number, num2: number, num3: number ): Promise => { 203 | try { 204 | const lcd = await getLCD(num1, num2) // will throw undefined in case LCD is not available for the values 205 | return await divide(lcd, num3) // throws "It is not possible to divide by 0" in case num3 is 0 206 | } catch(e) { 207 | // it is possible to recovery normal workflow here 208 | return someFallBackValue 209 | } 210 | } 211 | ``` 212 | 213 | ## API 214 | 215 | The interfaces contains an up to a certain degree shared API as well as specific ones for *Maybe* and *Result*. 216 | 217 | ### Commune 218 | 219 | #### Monad.prototype.then() 220 | 221 | Accordingly, applying the handlers produces a new Monadic as a container for the output of called function 222 | 223 | Signature for *Maybe* is: 224 | 225 | ```typescript 226 | Maybe.prototype.then( 227 | onJust?: (value: T) => TResult1 | Maybe, 228 | onNone?: () => Maybe 229 | ): Maybe 230 | ``` 231 | 232 | Signature for *Result* is: 233 | 234 | ```typescript 235 | Result.prototype.then( 236 | onSuccess?: (value: T) => TResult1 | IResult, 237 | onFailure?: (reason: E) => EResult2 | IResult 238 | ): Result 239 | ``` 240 | 241 | #### Monad.prototype.get() 242 | 243 | It returns the value enclosed inside a container. 244 | 245 | Signature for *Maybe* is: 246 | 247 | ```typescript 248 | Maybe.prototype.get(): T | undefined 249 | ``` 250 | 251 | Signature for *Result* is: 252 | 253 | ```typescript 254 | Result.prototype.get(): T | E 255 | ``` 256 | 257 | #### Monad.prototype.getOrElse() 258 | 259 | It returns the inclosed primary value or the one provided as an argument. 260 | 261 | Signature for *Maybe* is: 262 | 263 | ```typescript 264 | Maybe.prototype.getOrElse( value: T ): T 265 | ``` 266 | 267 | Signature for *Result* is: 268 | 269 | ```typescript 270 | Result.prototype.getOrElse( value: T ): T 271 | ``` 272 | 273 | #### Monad.prototype.getOrThrow() 274 | 275 | It returns the value for *Just* and *Success* and throws the throwable for *Failure*, *None* throws an *undefined*. 276 | 277 | ```typescript 278 | Monad.prototype.getOrThrow( value: T ): T 279 | ``` 280 | 281 | It might be useful for refactoring of a legacy codebase, since it simplifies implementation of interfaces with exceptions based error handling. 282 | 283 | ### Maybe 284 | 285 | *Maybe* itself represents a union type of Just and None. 286 | 287 | It is also a *smart factory* which turns Nullable object to *Just* or *None* accordingly. 288 | 289 | ```typescript 290 | Maybe(value: T | undefined | null) => Maybe 291 | ``` 292 | 293 | #### Just 294 | 295 | It represents the value of a specified type. It can be created via a factory which wraps the value with *Just* 296 | 297 | ```typescript 298 | Just(value: T) => Maybe 299 | ``` 300 | 301 | #### None 302 | 303 | It represents an absents of value with the specified type. It can be created via a factory with the specified type. 304 | 305 | ```typescript 306 | None() => Maybe 307 | ``` 308 | 309 | #### isJust 310 | 311 | It exists as a stand-alone function which checks whether an object of any type is *Just* 312 | 313 | ```typescript 314 | isJust(obj: any): obj is Just 315 | ``` 316 | 317 | Moreover *Maybe* has a method dedicated to the same goal. 318 | 319 | ```typescript 320 | Maybe.prototype.isJust(): obj is Just 321 | ``` 322 | 323 | #### isNone 324 | 325 | It exists as a stand-alone function which checks whether an object of any type is *None* 326 | 327 | ```typescript 328 | isNone(obj: any): obj is None 329 | ``` 330 | 331 | Moreover *Maybe* has a method dedicated to the same goal. 332 | 333 | ```typescript 334 | Maybe.prototype.isNone(): obj is Just 335 | ``` 336 | 337 | ### Result 338 | 339 | *Result* itself represents a union type of Success and Failure. 340 | 341 | It is also a *smart factory* which calls provided function and stores its output as *Success* or *Failure* accordingly. 342 | 343 | ```typescript 344 | Maybe(action: () => T | Result) => Result 345 | ``` 346 | 347 | #### Success 348 | 349 | It represents the value of a specified type. It can be created via a factory which wraps the value with *Success* 350 | 351 | ```typescript 352 | Success(value: T) => Result 353 | ``` 354 | 355 | #### Failure 356 | 357 | Represents an error which explains an absents of a value. It can be created via a factory with the specified type. 358 | 359 | ```typescript 360 | Failure(error: E) => Result 361 | ``` 362 | 363 | #### isSuccess 364 | 365 | It exists as a stand-alone function which checks whether an object of any type is *Success* 366 | 367 | ```typescript 368 | isSuccess(obj: any): obj is Success 369 | ``` 370 | 371 | Moreover, *Result* has a method dedicated to the same goal. 372 | 373 | ```typescript 374 | Result.prototype.isSuccess(): obj is Success 375 | ``` 376 | 377 | #### isFailure 378 | 379 | It exists as a stand-alone function which checks whether an object of any type is *Failure* 380 | 381 | ```typescript 382 | isFailure(obj: any): obj is Failure 383 | ``` 384 | 385 | Moreover, *Result* has a method dedicated to the same goal. 386 | 387 | ```typescript 388 | Result.prototype.isFailure(): obj is Failure 389 | ``` 390 | 391 | ## Contribution guidelines 392 | 393 | The project is based on *npm* eco-system. Therefore, development process is organized via *npm* scripts. 394 | 395 | For installation of dependencies run 396 | 397 | npm install 398 | 399 | To build application once 400 | 401 | npm run build 402 | 403 | To build an application and watch for changes of files 404 | 405 | npm run build:w 406 | 407 | To run tslint one time for CI 408 | 409 | npm run lint 410 | 411 | To unit tests in a watching mode are performed by 412 | 413 | npm run test 414 | 415 | To execute a test suit single time 416 | 417 | npm run test:once 418 | 419 | To execute a test suit single time with coverage report 420 | 421 | npm run test:c 422 | 423 | To execute a test suit single time with coverage report submitted to *coveralls* 424 | 425 | npm run test:ci 426 | 427 | Everybody is welcome to contribute and submit pull requests. Please communicate your ideas and suggestions via *issues*. 428 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amonad", 3 | "version": "2.0.1", 4 | "description": "Experimental implementation of Maybe and Result monads compatible with await.", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "prepack": "npm run build", 8 | "clean": "rm -rf ./dist", 9 | "release": "npm publish --access public", 10 | "build": "npm run clean && tsc -p tsconfig.json", 11 | "build:w": "tsc -p tsconfig.json -w", 12 | "lint": "tslint -c tslint.json 'src/**/*.ts'", 13 | "test": "mocha --recursive --require ts-node/register \"src/**/*.spec.ts\" --watch", 14 | "test:once": "mocha --require ts-node/register \"src/**/*.spec.ts\"", 15 | "test:c": "nyc npm run test:once", 16 | "test:ci": "nyc npm run test:once && nyc report --reporter=text-lcov | coveralls" 17 | }, 18 | "repository": { 19 | "url": "https://github.com/AIRTucha/amonad" 20 | }, 21 | "keywords": [ 22 | "typescript", 23 | "ts", 24 | "monad", 25 | "await", 26 | "maybe", 27 | "option", 28 | "fp", 29 | "async", 30 | "async-await", 31 | "functional-programming", 32 | "functional programming", 33 | "error-handling", 34 | "error handling", 35 | "async await", 36 | "result", 37 | "try", 38 | "try catch", 39 | "promise", 40 | "thenable", 41 | "monadic", 42 | "then", 43 | "functional" 44 | ], 45 | "author": { 46 | "name": "Alex T", 47 | "url": "https://github.com/AIRTucha" 48 | }, 49 | "license": "MIT", 50 | "nyc": { 51 | "include": [ 52 | "src/*.ts", 53 | "src/**/*.ts" 54 | ], 55 | "exclude": [ 56 | "node_modules", 57 | "typings", 58 | "src/**/*.spec.ts" 59 | ], 60 | "extension": [ 61 | ".ts" 62 | ], 63 | "require": [ 64 | "ts-node/register" 65 | ], 66 | "reporter": [ 67 | "json", 68 | "html", 69 | "text" 70 | ], 71 | "all": true 72 | }, 73 | "devDependencies": { 74 | "@types/chai": "^4.1.7", 75 | "@types/mocha": "^5.2.7", 76 | "@types/node": "^12.0.4", 77 | "chai": "^4.2.0", 78 | "coveralls": "^3.0.3", 79 | "mocha": "^6.1.4", 80 | "mocha-lcov-reporter": "^1.3.0", 81 | "nyc": "^14.1.1", 82 | "ts-node": "^8.2.0", 83 | "tslint": "^5.17.0", 84 | "typescript": "^3.7.2" 85 | } 86 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './maybe' 2 | export * from './result' -------------------------------------------------------------------------------- /src/maybe.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { Just, isJust, None, isNone, Maybe, isMaybe } from "./maybe" 4 | 5 | import { Success, Failure } from "./result" 6 | import { testNumber1, mapNumbersToNumber, mapNumberToString, testString1, mapStringToString, testNumber2 } from './testUtils' 7 | 8 | const throwIncorrectTypeIdentifiedType = () => { 9 | throw "Incorrectly identified type" 10 | } 11 | 12 | const assertUnchangedJust = ( monad: Maybe ) => 13 | expect( monad.get() ).to.be.eql( testNumber1 ) 14 | 15 | const assertUnchangedNone = ( monad: Maybe ) => 16 | expect( monad.get() ).to.be.undefined 17 | 18 | const assertFulfilledMapNumberToNumber = ( maybe: Maybe ) => 19 | expect( maybe.get() ).to.be.eql( mapNumbersToNumber( testNumber1 ) ) 20 | 21 | const assertFulfilledMapNumberToString = ( maybe: Maybe ) => 22 | expect( maybe.get() ).to.be.eql( mapNumberToString( testNumber1 ) ) 23 | 24 | const assertIsJust = ( maybe: Maybe ) => 25 | expect( maybe.isJust() ).to.be.true 26 | 27 | const assertIsNone = ( maybe: Maybe ) => 28 | expect( maybe.isNone() ).to.be.true 29 | 30 | const assertRejectedMapNumberToNumber = ( maybe: Maybe ) => 31 | expect( maybe.get() ).to.be.undefined 32 | 33 | const assertRejectedMapNumberToString = ( maybe: Maybe ) => 34 | expect( maybe.get() ).to.be.undefined 35 | 36 | const assertIgnoreOnReject = ( maybe: Maybe ) => 37 | assertIsJust( maybe ) && assertUnchangedJust( maybe ) 38 | 39 | const assertIgnoreOnFullfil = ( maybe: Maybe ) => 40 | assertIsNone( maybe ) && assertUnchangedNone( maybe ) 41 | 42 | const nothingToNumber = () => testNumber2 43 | 44 | describe( "Maybe", () => { 45 | describe( "Just", () => { 46 | const just = Just( testNumber1 ) 47 | 48 | describe( "isJust()", () => { 49 | 50 | it( "function", () => { 51 | if ( isJust( just ) ) { 52 | const value: number = just.get() 53 | expect( value ).to.be.eql( testNumber1 ) 54 | } else 55 | throwIncorrectTypeIdentifiedType() 56 | } ) 57 | 58 | it( "method", () => { 59 | if ( just.isJust() ) { 60 | const value: number = just.get() 61 | expect( value ).to.be.eql( testNumber1 ) 62 | } else 63 | throwIncorrectTypeIdentifiedType() 64 | } ) 65 | } ) 66 | 67 | describe( "isNone()", () => { 68 | 69 | it( "function", () => { 70 | if ( isNone( just ) ) 71 | throwIncorrectTypeIdentifiedType() 72 | else { 73 | const value: number = just.get() 74 | expect( value ).to.be.eql( testNumber1 ) 75 | } 76 | } ) 77 | 78 | it( "method", () => { 79 | if ( just.isNone() ) 80 | throwIncorrectTypeIdentifiedType() 81 | else { 82 | const value: number = just.get() 83 | expect( value ).to.be.eql( testNumber1 ) 84 | } 85 | } ) 86 | } ) 87 | 88 | describe( "then()", () => { 89 | let monad!: Maybe 90 | 91 | beforeEach( () => { 92 | monad = Just( testNumber1 ) 93 | } ) 94 | 95 | describe( "onfulfilled mapped to", () => { 96 | 97 | it( 'value of the same type', () => { 98 | const result = monad 99 | .then( mapNumbersToNumber ) 100 | assertIsJust( result ) 101 | return assertFulfilledMapNumberToNumber( result ) 102 | } ) 103 | 104 | it( 'value of a different type', () => { 105 | const result = monad 106 | .then( mapNumberToString ) 107 | assertIsJust( result ) 108 | return assertFulfilledMapNumberToString( result ) 109 | } ) 110 | 111 | describe( "monad of", () => { 112 | it( 'the same type', () => { 113 | const result = monad 114 | .then( value => Just( mapNumberToString( value ) ) ) 115 | assertIsJust( result ) 116 | return assertFulfilledMapNumberToString( result ) 117 | } ) 118 | 119 | it( 'an opposite type', () => { 120 | const result = monad 121 | .then( value => None() ) 122 | assertIsNone( result ) 123 | return assertRejectedMapNumberToString( result ) 124 | } ) 125 | 126 | it( 'an different type', async () => { 127 | const result = monad 128 | .then( value => Promise.resolve( mapNumberToString( value ) ) ) 129 | assertIsJust( result ) 130 | const promise = await result 131 | const maybe = await promise 132 | expect( maybe ).to.be.eql( mapNumberToString( testNumber1 ) ) 133 | } ) 134 | } ) 135 | } ) 136 | 137 | describe( "onrejected ignore mapping to", () => { 138 | 139 | describe( "monad of", () => { 140 | it( 'the same type', () => { 141 | const result = monad 142 | .then( 143 | undefined, 144 | () => Just( testNumber2 ) 145 | ) 146 | return assertIgnoreOnReject( result ) 147 | } ) 148 | 149 | it( 'an opposite type', () => { 150 | const result = monad 151 | .then( 152 | undefined, 153 | () => None() 154 | ) 155 | return assertIgnoreOnReject( result ) 156 | } ) 157 | } ) 158 | } ) 159 | } ) 160 | } ) 161 | 162 | describe( "None", () => { 163 | const none = None() 164 | 165 | describe( "isJust()", () => { 166 | 167 | it( "function", () => { 168 | if ( isJust( none ) ) 169 | throwIncorrectTypeIdentifiedType() 170 | else 171 | expect( none.get() ).to.be.undefined 172 | } ) 173 | 174 | it( "method", () => { 175 | if ( none.isJust() ) 176 | throwIncorrectTypeIdentifiedType() 177 | else 178 | expect( none.get() ).to.be.undefined 179 | } ) 180 | } ) 181 | 182 | describe( "isNone()", () => { 183 | 184 | it( "function", () => { 185 | if ( isNone( none ) ) 186 | expect( none.get() ).to.be.undefined 187 | else 188 | throwIncorrectTypeIdentifiedType() 189 | } ) 190 | 191 | it( "method", () => { 192 | if ( none.isNone() ) 193 | expect( none.get() ).to.be.undefined 194 | else 195 | throwIncorrectTypeIdentifiedType() 196 | } ) 197 | } ) 198 | 199 | describe( "then()", () => { 200 | let monad: Maybe 201 | 202 | beforeEach( () => { 203 | monad = None() 204 | } ) 205 | 206 | describe( "onfulfilled ignore mapping to", () => { 207 | 208 | it( 'value of the same type', () => { 209 | const result = monad 210 | .then( mapNumbersToNumber ) 211 | assertIgnoreOnFullfil( result ) 212 | } ) 213 | 214 | it( 'value of a different type', () => { 215 | const result = monad 216 | .then( mapNumberToString ) 217 | assertIgnoreOnFullfil( result as any ) 218 | } ) 219 | 220 | describe( "monad of", () => { 221 | 222 | it( 'the same type', () => { 223 | const result = monad 224 | .then( value => None() ) 225 | assertIgnoreOnFullfil( result ) 226 | } ) 227 | 228 | it( 'an opposite type', () => { 229 | const result = monad 230 | .then( value => Just( mapNumbersToNumber( value ) ) ) 231 | assertIgnoreOnFullfil( result ) 232 | } ) 233 | 234 | it( 'an different type', () => { 235 | const result = monad 236 | .then( value => Promise.resolve( mapNumbersToNumber( value ) ) ) 237 | assertIgnoreOnFullfil( result as any ) 238 | } ) 239 | } ) 240 | } ) 241 | 242 | describe( "onrejected mapped to", () => { 243 | 244 | describe( "monad of", () => { 245 | it( 'the same monad', () => { 246 | const result = monad 247 | .then( 248 | undefined, 249 | () => None() 250 | ) 251 | assertIsNone( result ) 252 | return assertRejectedMapNumberToNumber( result ) 253 | } ) 254 | 255 | it( 'an opposite monad', () => { 256 | const result = monad 257 | .then( 258 | undefined, 259 | () => Just( mapNumbersToNumber( testNumber1 ) ) 260 | ) 261 | assertIsJust( result ) 262 | return assertFulfilledMapNumberToNumber( result ) 263 | } ) 264 | } ) 265 | } ) 266 | } ) 267 | } ) 268 | 269 | describe( "Maybe with", () => { 270 | 271 | it( "value", () => { 272 | expect( Maybe( testNumber1 ) ).to.be.eql( Just( testNumber1 ) ) 273 | } ) 274 | 275 | it( "undefined", () => { 276 | expect( Maybe( undefined ) ).to.be.eql( None() ) 277 | } ) 278 | 279 | describe( "isMaybe()", () => { 280 | it( "Just", () => { 281 | expect( isMaybe( Just( testNumber1 ) ) ).to.be.true 282 | } ) 283 | 284 | it( "None", () => { 285 | expect( isMaybe( None() ) ).to.be.true 286 | } ) 287 | 288 | it( "string", () => { 289 | expect( isMaybe( "None" ) ).to.be.false 290 | } ) 291 | 292 | it( "number", () => { 293 | expect( isMaybe( 10 ) ).to.be.false 294 | } ) 295 | 296 | it( "boolean", () => { 297 | expect( isMaybe( 10 ) ).to.be.false 298 | } ) 299 | 300 | it( "object", () => { 301 | expect( isMaybe( {} ) ).to.be.false 302 | } ) 303 | 304 | it( "Array", () => { 305 | expect( isMaybe( new Array() ) ).to.be.false 306 | } ) 307 | 308 | it( "Promise", () => { 309 | expect( isMaybe( Promise.resolve() ) ).to.be.false 310 | } ) 311 | 312 | it( "Success", () => { 313 | expect( isMaybe( Success( testNumber1 ) ) ).to.be.false 314 | } ) 315 | 316 | it( "Failure", () => { 317 | expect( isMaybe( Failure( "" ) ) ).to.be.false 318 | } ) 319 | } ) 320 | } ) 321 | 322 | describe( "await", () => { 323 | describe( "Just", () => { 324 | 325 | it( "function correctly", async () => { 326 | const value = await Just( testNumber1 ) 327 | expect( value ).to.be.eql( testNumber1 ) 328 | } ) 329 | 330 | it( "with None", async () => { 331 | try { 332 | const svalue = await Just( testNumber1 ) 333 | const fvalue = await None() 334 | throw "The line should not be reached" 335 | } catch ( e ) { 336 | expect( e ).to.be.eql( undefined ) 337 | } 338 | } ) 339 | 340 | it( 'getOrThrow() throw correct error', () => { 341 | const error = new Error( testString1 ) 342 | expect( 343 | () => None().getOrThrow() 344 | ).to.throws( "The value is None" ) 345 | } ) 346 | 347 | describe( "Promise", () => { 348 | describe( "resolved", () => { 349 | 350 | it( "before", async () => { 351 | const pValue = await Promise.resolve( testNumber1 ) 352 | const value = await Just( testNumber2 ) 353 | expect( pValue ).to.be.eql( testNumber1 ) 354 | expect( value ).to.be.eql( testNumber2 ) 355 | } ) 356 | 357 | it( "after", async () => { 358 | const value = await Just( testNumber2 ) 359 | const pValue = await Promise.resolve( testNumber1 ) 360 | expect( pValue ).to.be.eql( testNumber1 ) 361 | expect( value ).to.be.eql( testNumber2 ) 362 | } ) 363 | } ) 364 | describe( "rejected", () => { 365 | 366 | it( "before", async () => { 367 | try { 368 | const pValue = await Promise.reject( testNumber1 ) 369 | const value = await Just( testNumber2 ) 370 | throw "The line should not be reached" 371 | } catch ( e ) { 372 | expect( e ).to.be.eql( testNumber1 ) 373 | } 374 | } ) 375 | 376 | it( "after", async () => { 377 | try { 378 | const value = await Just( testString1 ) 379 | const pValue = await Promise.reject( testNumber1 ) 380 | throw "The line should not be reached" 381 | } catch ( e ) { 382 | expect( e ).to.be.eql( testNumber1 ) 383 | } 384 | } ) 385 | } ) 386 | } ) 387 | 388 | } ) 389 | 390 | describe( "None", () => { 391 | 392 | it( "throws value", async () => { 393 | try { 394 | await None() 395 | throw "The line should not be reached" 396 | } catch ( e ) { 397 | expect( e ).to.be.eql( undefined ) 398 | } 399 | } ) 400 | 401 | it( "with Just", async () => { 402 | try { 403 | const fvalue = await None() 404 | const svalue = await Just( testNumber1 ) 405 | throw "The line should not be reached" 406 | } catch ( e ) { 407 | expect( e ).to.be.eql( undefined ) 408 | } 409 | } ) 410 | 411 | describe( "Promise", () => { 412 | describe( "resolved", () => { 413 | 414 | it( "before", async () => { 415 | try { 416 | const value = await None() 417 | const pValue = await Promise.resolve( testNumber1 ) 418 | throw "The line should not be reached" 419 | } catch ( e ) { 420 | expect( e ).to.be.eql( undefined ) 421 | } 422 | } ) 423 | 424 | it( "after", async () => { 425 | try { 426 | const pValue = await Promise.resolve( testNumber1 ) 427 | const value = await None() 428 | throw "The line should not be reached" 429 | } catch ( e ) { 430 | expect( e ).to.be.eql( undefined ) 431 | } 432 | } ) 433 | } ) 434 | describe( "rejected", () => { 435 | 436 | it( "before", async () => { 437 | try { 438 | const pValue = await Promise.reject( testNumber1 ) 439 | const value = await None() 440 | throw "The line should not be reached" 441 | } catch ( e ) { 442 | expect( e ).to.be.eql( testNumber1 ) 443 | } 444 | } ) 445 | 446 | it( "after", async () => { 447 | try { 448 | const value = await None() 449 | const pValue = await Promise.reject( testNumber1 ) 450 | throw "The line should not be reached" 451 | } catch ( e ) { 452 | expect( e ).to.be.eql( undefined ) 453 | } 454 | } ) 455 | } ) 456 | } ) 457 | } ) 458 | } ) 459 | } ) -------------------------------------------------------------------------------- /src/maybe.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CJustSuccess, CNoneFailure, fulfilled, rejected } from "./monad" 3 | import { Thenable } from './thenable' 4 | 5 | const thenErrorMsg = "Maybe.then() is should be full filled by monad decorator." 6 | 7 | /** 8 | * Define properties specific to Maybe monad 9 | * 10 | * @note then() is not defined, since it is attached by decorator 11 | */ 12 | interface IMaybe extends Thenable { 13 | /** 14 | * Implementation of PromiseLike.then() for proper functioning of await 15 | * @param onfulfilled Handler for fulfilled value 16 | * @param onrejected Handler for onrejected value 17 | * @return PromiseLike object which inclose new value 18 | */ 19 | then( 20 | onfulfilled?: ( ( value: T ) => TResult1 | PromiseLike ) | undefined | null, 21 | onrejected?: ( ( reason: any ) => TResult2 | PromiseLike ) | undefined | null 22 | ): PromiseLike 23 | /** 24 | * Accordingly apply the handlers produces a new Maybe as container for the output of called function 25 | * @param onJust Handler for fulfilled value 26 | * @param onNone Handler for onrejected value 27 | * @return Maybe object which inclose new value 28 | */ 29 | then( 30 | onJust?: ( ( value: T ) => TResult1 | IMaybe | void ), 31 | onNone?: ( () => IMaybe | void ) 32 | ): Maybe 33 | /** 34 | * @returns Wether this is None 35 | */ 36 | isNone(): this is None 37 | /** 38 | * @returns Wether this is Just 39 | */ 40 | isJust(): this is Just 41 | } 42 | 43 | /** 44 | * Representation of a value 45 | */ 46 | export type Just = CJust 47 | 48 | /** 49 | * Representation of an absent value 50 | */ 51 | export type None = CNone 52 | 53 | /** 54 | * Representation of a value which might not exist 55 | */ 56 | export type Maybe = Just | None 57 | 58 | /** 59 | * @param obj Object which might be Maybe 60 | * @returns Wether the object is Maybe 61 | */ 62 | export const isMaybe = ( obj: any ): obj is Maybe => 63 | isNone( obj ) || isJust( obj ) 64 | 65 | /** 66 | * @param obj Object which might be None 67 | * @returns Wether the object is None 68 | */ 69 | export const isNone = ( obj: any ): obj is None => 70 | obj instanceof CNone 71 | 72 | /** 73 | * @param obj Object which might be Just 74 | * @returns Wether the object is Just 75 | */ 76 | export const isJust = ( obj: any ): obj is Just => 77 | obj instanceof CJust 78 | 79 | /** 80 | * @param value Value which is going to be inclosed inside of Maybe 81 | * @returns Maybe with inclosed value as Just 82 | */ 83 | export const Just = 84 | ( value: T ): Maybe => new CJust( value ) as any 85 | 86 | /** 87 | * @returns Empty Maybe as None 88 | */ 89 | export const None = 90 | (): Maybe => new CNone() as any 91 | 92 | /** 93 | * @param value Nullable value which is going to be inclosed inside of Maybe 94 | * @return Maybe of Just or None depends if the value was defined 95 | */ 96 | export const Maybe = ( value: T | undefined | null ): Maybe => 97 | value !== null && value !== undefined 98 | ? 99 | Just( value ) 100 | : 101 | None() 102 | 103 | /** 104 | * Container which represents value 105 | */ 106 | class CJust extends CJustSuccess implements IMaybe { 107 | 108 | @fulfilled( isMaybe ) 109 | then( 110 | onJust?: ( value: T ) => TResult1 | Maybe, 111 | onNone?: () => Maybe 112 | ): Maybe { 113 | throw new Error( thenErrorMsg ) 114 | } 115 | 116 | isNone(): this is None { 117 | return false 118 | } 119 | 120 | isJust(): this is Just { 121 | return true 122 | } 123 | } 124 | 125 | /** 126 | * Container which represents lack of value 127 | */ 128 | class CNone extends CNoneFailure implements IMaybe { 129 | constructor() { 130 | super( undefined ) 131 | } 132 | 133 | @rejected( isMaybe ) 134 | then( 135 | onJust?: ( value: T ) => TResult1 | Maybe, 136 | onNone?: () => Maybe 137 | ): Maybe { 138 | throw new Error( thenErrorMsg ) 139 | } 140 | 141 | isNone(): this is None { 142 | return true 143 | } 144 | 145 | isJust(): this is Just { 146 | return false 147 | } 148 | 149 | getOrThrow(): T { 150 | // override default behavior since 151 | // 'undefined' should not be thrown 152 | throw "The value is None" 153 | } 154 | } 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/monad.spec.ts: -------------------------------------------------------------------------------- 1 | import { CJustSuccess, CNoneFailure, fulfilled, rejected } from './monad' 2 | import { expect } from 'chai' 3 | import { testNumber1, testNumber2, testString1, mapNumberToString, mapNumbersToNumber, mapStringToString } from './testUtils' 4 | 5 | // *** Test logic of CJustSuccess and CNoneFailure classes *** 6 | 7 | const assertIsCJustSuccess = ( result: any ) => 8 | expect( result ).to 9 | .be 10 | .instanceOf( CJustSuccess ) 11 | 12 | const assertIsCNoneFailure = ( result: any ) => 13 | expect( result ) 14 | .to 15 | .be 16 | .instanceOf( CNoneFailure ) 17 | 18 | const assertUnchangedCJustSuccess = ( monad: any ) => 19 | monad.then( v => 20 | expect( v ) 21 | .to 22 | .eql( testNumber1 ) 23 | ) 24 | 25 | const assertUnchangedCNoneFailure = ( monad: any ) => 26 | monad.then( 27 | undefined, 28 | v => { 29 | expect( v ) 30 | .to 31 | .eql( testNumber1 ) 32 | return Promise.resolve() 33 | } ) 34 | 35 | describe( 'CJustSuccess', () => { 36 | 37 | it( 'get() returns correct primary value', () => { 38 | expect( 39 | new CJustSuccess( testNumber1 ).get() 40 | ).eql( testNumber1 ) 41 | } ) 42 | 43 | it( 'getOrElse() return correct value', () => { 44 | expect( 45 | new CJustSuccess( testNumber1 ).getOrElse( testNumber2 ) 46 | ).eql( testNumber1 ) 47 | } ) 48 | 49 | it( 'getOrThrow() return correct value', () => { 50 | expect( 51 | new CJustSuccess( testNumber1 ).getOrThrow() 52 | ).eql( testNumber1 ) 53 | } ) 54 | } ) 55 | 56 | describe( 'CNoneFailure', () => { 57 | 58 | it( 'get() return correct secondary value', () => { 59 | expect( 60 | new CNoneFailure( testString1 ).get() 61 | ).eql( testString1 ) 62 | } ) 63 | 64 | it( 'getOrElse() return fallback value', () => { 65 | expect( 66 | new CNoneFailure( testNumber1 ).getOrElse( testNumber2 ) 67 | ).eql( testNumber2 ) 68 | } ) 69 | } ) 70 | -------------------------------------------------------------------------------- /src/monad.ts: -------------------------------------------------------------------------------- 1 | import { Thenable, isThenable } from './thenable' 2 | 3 | /** 4 | * Container of with a fallback value 5 | */ 6 | interface Gettable { 7 | /** 8 | * @returns Object of type T if available, object of type E as fallback 9 | */ 10 | get(): T | E 11 | /** 12 | * @param value Fallback value in case T is not available 13 | * @return Stored value of T or a provided fallback 14 | */ 15 | getOrElse( value: T ): T 16 | /** 17 | * @returns Object of type T if available or throws an error which described absents of value 18 | * @note It is not right way to use the API, 19 | * but it might be handy refactoring of an old codebase 20 | */ 21 | getOrThrow(): T 22 | } 23 | 24 | 25 | export function fulfilled( isMonad: ( value: any ) => boolean ) { 26 | return function ( target: any, propertyKey: string, descriptor: TypedPropertyDescriptor ) { 27 | descriptor.value = function ( 28 | this: any, 29 | onfulfilled?: ( ( value: T ) => TResult1 | Thenable ) | undefined | null, 30 | onrejected?: ( ( reason: any ) => TResult2 | Thenable ) | undefined | null 31 | ): Thenable { 32 | if ( onfulfilled ) { 33 | const value = onfulfilled( this.v ) 34 | return isMonad( value ) ? value : new target.constructor( value ) as any 35 | } else 36 | return this as any 37 | } as any 38 | } 39 | } 40 | 41 | export function rejected( isMonad: ( value: any ) => boolean ) { 42 | return function ( target: any, propertyKey: string, descriptor: TypedPropertyDescriptor ) { 43 | descriptor.value = function ( 44 | this: any, 45 | onfulfilled?: ( ( value: T ) => TResult1 | Thenable ) | undefined | null, 46 | onrejected?: ( ( reason: any ) => TResult2 | Thenable ) | undefined | null 47 | ): Thenable { 48 | if ( onrejected ) { 49 | const value = onrejected( this.v ) 50 | return isMonad( value ) ? value : new target.constructor( value ) as any 51 | } else 52 | return this as any 53 | } as any 54 | } 55 | } 56 | 57 | /** 58 | * Container which might represent primary value, base for Just and Success 59 | */ 60 | export class CJustSuccess { 61 | /** 62 | * @param v Primary stored value 63 | */ 64 | constructor( private v: T ) { } 65 | 66 | get(): T { 67 | return this.v 68 | } 69 | 70 | getOrElse( value: T ): T { 71 | return this.v 72 | } 73 | 74 | getOrThrow(): T { 75 | return this.v 76 | } 77 | } 78 | 79 | /** 80 | * Container which might represent secondary value, base for None and Failure 81 | */ 82 | export class CNoneFailure { 83 | /** 84 | * @param v Secondary stored value 85 | */ 86 | constructor( protected v: E ) { } 87 | 88 | getOrElse( value: T ): T { 89 | return value 90 | } 91 | 92 | get() { 93 | return this.v 94 | } 95 | 96 | getOrThrow(): T { 97 | this.v 98 | throw this.v 99 | } 100 | } -------------------------------------------------------------------------------- /src/result.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { Just, None, Maybe } from "./maybe" 4 | 5 | import { Success, isSuccess, isFailure, Failure, Result, isResult } from "./result" 6 | import { testNumber1, mapNumbersToNumber, mapNumberToString, testString1, mapStringToString, testNumber2, testString2 } from './testUtils' 7 | 8 | const testValue = 1 9 | const errorMsg = "testError" 10 | 11 | const throwIncorrectTypeIdentifiedType = () => { 12 | throw "Incorrectly identified type" 13 | } 14 | 15 | const assertUnchangedJust = ( monad: Result ) => 16 | expect( monad.get() ).to.be.eql( testNumber1 ) 17 | 18 | const assertUnchangedNone = ( monad: Result ) => 19 | expect( monad.get() ).to.be.eql( testString1 ) 20 | 21 | const assertFulfilledMapNumberToNumber = ( maybe: Result ) => 22 | expect( maybe.get() ).to.be.eql( mapNumbersToNumber( testNumber1 ) ) 23 | 24 | const assertFulfilledMapNumberToString = ( maybe: Result ) => 25 | expect( maybe.get() ).to.be.eql( mapNumberToString( testNumber1 ) ) 26 | 27 | const assertFulfilledMapStringToString = ( maybe: Result ) => 28 | expect( maybe.get() ).to.be.eql( mapStringToString( testString1 ) ) 29 | 30 | const assertIsJust = ( maybe: Result ) => 31 | expect( maybe.isSuccess() ).to.be.true 32 | 33 | const assertIsNone = ( maybe: Result ) => 34 | expect( maybe.isFailure() ).to.be.true 35 | 36 | const assertRejectedMapStringToString = ( maybe: Result ) => 37 | expect( maybe.get() ).to.be.eql( mapStringToString( testString1 ) ) 38 | 39 | const assertRejectedMapStringToError = ( maybe: Result ) => { 40 | if ( maybe.isFailure() ) { 41 | const error = maybe.get() 42 | expect( error ).to.be.instanceOf( Error ) 43 | expect( error.message ).to.be.eql( testString1 ) 44 | } 45 | } 46 | 47 | 48 | const assertRejectedMapNumberToString = ( maybe: Result ) => 49 | expect( maybe.get() ).to.be.eql( testNumber1 ) 50 | 51 | const assertIgnoreOnReject = ( maybe: Result ) => 52 | assertIsJust( maybe ) && assertUnchangedJust( maybe ) 53 | 54 | const assertIgnoreOnFullfil = ( maybe: Result ) => 55 | assertIsNone( maybe ) && assertUnchangedNone( maybe ) 56 | 57 | describe( "Result", () => { 58 | describe( "Success", () => { 59 | const success = Success( testValue ) 60 | 61 | describe( "isSuccess()", () => { 62 | 63 | it( "function", () => { 64 | if ( isSuccess( success ) ) { 65 | const value: number = success.get() 66 | expect( value ).to.be.eql( testValue ) 67 | } else 68 | throwIncorrectTypeIdentifiedType() 69 | } ) 70 | 71 | it( "method", () => { 72 | if ( success.isSuccess() ) { 73 | const value: number = success.get() 74 | expect( value ).to.be.eql( testValue ) 75 | } else 76 | throwIncorrectTypeIdentifiedType() 77 | } ) 78 | } ) 79 | 80 | describe( "isFailure()", () => { 81 | 82 | it( "function", () => { 83 | if ( isFailure( success ) ) 84 | throwIncorrectTypeIdentifiedType() 85 | else { 86 | const value: number = success.get() 87 | expect( value ).to.be.eql( testValue ) 88 | } 89 | } ) 90 | 91 | it( "method", () => { 92 | if ( success.isFailure() ) 93 | throwIncorrectTypeIdentifiedType() 94 | else { 95 | const value: number = success.get() 96 | expect( value ).to.be.eql( testValue ) 97 | } 98 | } ) 99 | } ) 100 | 101 | describe( "then()", () => { 102 | let monad!: Result 103 | 104 | beforeEach( () => { 105 | monad = Success( testNumber1 ) 106 | } ) 107 | 108 | describe( "onfulfilled mapped to", () => { 109 | 110 | it( 'value of the same type', () => { 111 | const result = monad 112 | .then( mapNumbersToNumber ) 113 | assertIsJust( result ) 114 | return assertFulfilledMapNumberToNumber( result ) 115 | } ) 116 | 117 | it( 'value of a different type', () => { 118 | const result = monad 119 | .then( mapNumberToString ) 120 | assertIsJust( result ) 121 | return assertFulfilledMapNumberToString( result ) 122 | } ) 123 | 124 | describe( "monad of", () => { 125 | it( 'the same type', () => { 126 | const result = monad 127 | .then( value => Success( mapNumberToString( value ) ) ) 128 | assertIsJust( result ) 129 | return assertFulfilledMapNumberToString( result ) 130 | } ) 131 | 132 | it( 'an opposite type', () => { 133 | const result = monad 134 | .then( value => Failure( mapNumberToString( value ) ) ) 135 | assertIsNone( result ) 136 | return assertFulfilledMapNumberToString( result ) 137 | } ) 138 | 139 | it( 'an different type', async () => { 140 | const result = monad 141 | .then( value => Promise.resolve( mapNumberToString( value ) ) ) 142 | assertIsJust( result ) 143 | const promise = await result 144 | const maybe = await promise 145 | expect( maybe ).to.be.eql( mapNumberToString( testNumber1 ) ) 146 | } ) 147 | } ) 148 | } ) 149 | 150 | describe( "onrejected ignore mapping to", () => { 151 | it( 'value of the same type', () => { 152 | const result = monad 153 | .then( 154 | undefined, 155 | mapStringToString 156 | ) 157 | return assertIgnoreOnReject( result ) 158 | } ) 159 | 160 | it( 'value of a different type', () => { 161 | const result = monad 162 | .then( 163 | undefined, 164 | mapStringToString 165 | ) 166 | return assertIgnoreOnReject( result ) 167 | } ) 168 | 169 | describe( "monad of", () => { 170 | it( 'the same type', () => { 171 | const result: Result = monad 172 | .then( 173 | undefined, 174 | value => Success( mapStringToString( value ) ) 175 | ) 176 | return assertIgnoreOnReject( result ) 177 | } ) 178 | 179 | it( 'an opposite type', () => { 180 | const result = monad 181 | .then( 182 | undefined, 183 | value => Failure( value ) 184 | ) 185 | return assertIgnoreOnReject( result ) 186 | } ) 187 | } ) 188 | } ) 189 | } ) 190 | } ) 191 | 192 | describe( "Failure", () => { 193 | const failure = Failure( errorMsg ) 194 | 195 | describe( "isSuccess()", () => { 196 | 197 | it( "function", () => { 198 | if ( isSuccess( failure ) ) 199 | throwIncorrectTypeIdentifiedType() 200 | else 201 | expect( failure.get() ).to.be.eql( errorMsg ) 202 | } ) 203 | 204 | it( "method", () => { 205 | if ( failure.isSuccess() ) 206 | throwIncorrectTypeIdentifiedType() 207 | else 208 | expect( failure.get() ).to.be.eql( errorMsg ) 209 | } ) 210 | } ) 211 | 212 | describe( "isFailure()", () => { 213 | 214 | it( "function", () => { 215 | if ( isFailure( failure ) ) 216 | expect( failure.get() ).to.be.eql( errorMsg ) 217 | else 218 | throwIncorrectTypeIdentifiedType() 219 | } ) 220 | 221 | it( "method", () => { 222 | if ( failure.isFailure() ) 223 | expect( failure.get() ).to.be.eql( errorMsg ) 224 | else 225 | throwIncorrectTypeIdentifiedType() 226 | } ) 227 | } ) 228 | 229 | describe( "then()", () => { 230 | let monad: Result 231 | 232 | beforeEach( () => { 233 | monad = Failure( testString1 ) 234 | } ) 235 | 236 | describe( "onfulfilled ignore mapping to", () => { 237 | 238 | it( 'value of the same type', () => { 239 | const result = monad 240 | .then( mapNumbersToNumber ) 241 | assertIgnoreOnFullfil( result ) 242 | } ) 243 | 244 | it( 'value of a different type', () => { 245 | const result = monad 246 | .then( mapNumberToString ) 247 | assertIgnoreOnFullfil( result ) 248 | } ) 249 | 250 | describe( "monad of", () => { 251 | 252 | it( 'the same type', () => { 253 | const result = monad 254 | .then( value => Failure( mapNumberToString( value ) ) ) 255 | assertIgnoreOnFullfil( result ) 256 | } ) 257 | 258 | it( 'an opposite type', () => { 259 | const result = monad 260 | .then( value => Success( mapNumbersToNumber( value ) ) ) 261 | assertIgnoreOnFullfil( result ) 262 | } ) 263 | 264 | it( 'an different type', () => { 265 | const result = monad 266 | .then( value => Promise.resolve( mapNumbersToNumber( value ) ) as any ) 267 | assertIgnoreOnFullfil( result ) 268 | } ) 269 | } ) 270 | } ) 271 | 272 | describe( "onrejected mapped to", () => { 273 | 274 | it( 'value of the same type', () => { 275 | const result = monad 276 | .then( 277 | undefined, 278 | mapStringToString 279 | ) 280 | assertIsNone( result ) 281 | return assertRejectedMapStringToString( result ) 282 | } ) 283 | 284 | it( 'value of a different type', () => { 285 | const result = monad 286 | .then( 287 | undefined, 288 | value => new Error( value ) 289 | ) 290 | assertIsNone( result ) 291 | return assertRejectedMapStringToError( result ) 292 | } ) 293 | 294 | describe( "monad of", () => { 295 | it( 'the same monad', () => { 296 | const result = monad 297 | .then( 298 | undefined, 299 | value => Failure( mapStringToString( value ) ) 300 | ) 301 | assertIsNone( result ) 302 | return assertRejectedMapStringToString( result ) 303 | } ) 304 | 305 | it( 'an opposite monad', () => { 306 | const result = monad 307 | .then( 308 | undefined, 309 | value => Success( mapStringToString( value ) ) 310 | ) 311 | assertIsJust( result ) 312 | return assertFulfilledMapStringToString( result ) 313 | } ) 314 | } ) 315 | } ) 316 | } ) 317 | } ) 318 | 319 | describe( "Result with", () => { 320 | 321 | it( "value", () => { 322 | expect( Result( () => testValue ) ).to.be.eql( Success( testValue ) ) 323 | } ) 324 | 325 | it( "Success", () => { 326 | expect( Result( () => Success( testValue ) ) ).to.be.eql( Success( testValue ) ) 327 | } ) 328 | 329 | it( "Failure", () => { 330 | expect( Result( () => Failure( errorMsg ) ) ).to.be.eql( Failure( errorMsg ) ) 331 | } ) 332 | 333 | it( "throw", () => { 334 | expect( Result( () => { throw errorMsg } ) ).to.be.eql( Failure( errorMsg ) ) 335 | } ) 336 | 337 | describe( "isResult()", () => { 338 | it( "Success", () => { 339 | expect( isResult( Success( testValue ) ) ).to.be.true 340 | } ) 341 | 342 | it( "Failure", () => { 343 | expect( isResult( Failure( errorMsg ) ) ).to.be.true 344 | } ) 345 | 346 | it( "string", () => { 347 | expect( isResult( "Failure" ) ).to.be.false 348 | } ) 349 | 350 | it( "number", () => { 351 | expect( isResult( 10 ) ).to.be.false 352 | } ) 353 | 354 | it( "boolean", () => { 355 | expect( isResult( 10 ) ).to.be.false 356 | } ) 357 | 358 | it( "object", () => { 359 | expect( isResult( {} ) ).to.be.false 360 | } ) 361 | 362 | it( "Array", () => { 363 | expect( isResult( new Array() ) ).to.be.false 364 | } ) 365 | 366 | it( "Promise", () => { 367 | expect( isResult( Promise.resolve() ) ).to.be.false 368 | } ) 369 | 370 | it( "Just", () => { 371 | expect( isResult( Just( testValue ) ) ).to.be.false 372 | } ) 373 | 374 | it( "None", () => { 375 | expect( isResult( None() ) ).to.be.false 376 | } ) 377 | } ) 378 | } ) 379 | 380 | 381 | describe( "await", () => { 382 | describe( "Success", () => { 383 | 384 | it( "function correctly", async () => { 385 | const value = await Success( testNumber1 ) 386 | expect( value ).to.be.eql( testNumber1 ) 387 | } ) 388 | 389 | it( "with Failure", async () => { 390 | try { 391 | const svalue = await Success( testNumber1 ) 392 | const fvalue = await Failure( testString1 ) 393 | throw "The line should not be reached" 394 | } catch ( e ) { 395 | expect( e ).to.be.eql( testString1 ) 396 | } 397 | } ) 398 | 399 | it( 'getOrThrow() throw correct error', () => { 400 | const error = new Error( testString1 ) 401 | expect( 402 | () => Failure( error ).getOrThrow() 403 | ).to.throws( error ) 404 | } ) 405 | 406 | describe( "Promise", () => { 407 | describe( "resolved", () => { 408 | 409 | it( "before", async () => { 410 | const pValue = await Promise.resolve( testNumber1 ) 411 | const value = await Success( testNumber2 ) 412 | expect( pValue ).to.be.eql( testNumber1 ) 413 | expect( value ).to.be.eql( testNumber2 ) 414 | } ) 415 | 416 | it( "after", async () => { 417 | const value = await Success( testNumber2 ) 418 | const pValue = await Promise.resolve( testNumber1 ) 419 | expect( pValue ).to.be.eql( testNumber1 ) 420 | expect( value ).to.be.eql( testNumber2 ) 421 | } ) 422 | } ) 423 | describe( "rejected", () => { 424 | 425 | it( "before", async () => { 426 | try { 427 | const pValue = await Promise.reject( testNumber1 ) 428 | const value = await Success( testNumber2 ) 429 | throw "The line should not be reached" 430 | } catch ( e ) { 431 | expect( e ).to.be.eql( testNumber1 ) 432 | } 433 | } ) 434 | 435 | it( "after", async () => { 436 | try { 437 | const value = await Success( testString1 ) 438 | const pValue = await Promise.reject( testNumber1 ) 439 | throw "The line should not be reached" 440 | } catch ( e ) { 441 | expect( e ).to.be.eql( testNumber1 ) 442 | } 443 | } ) 444 | } ) 445 | } ) 446 | 447 | } ) 448 | 449 | describe( "Failure", () => { 450 | 451 | it( "throws value", async () => { 452 | try { 453 | await Failure( testString1 ) 454 | throw "The line should not be reached" 455 | } catch ( e ) { 456 | expect( e ).to.be.eql( testString1 ) 457 | } 458 | } ) 459 | 460 | it( "with Success", async () => { 461 | try { 462 | const fvalue = await Failure( testString1 ) 463 | const svalue = await Success( testNumber1 ) 464 | throw "The line should not be reached" 465 | } catch ( e ) { 466 | expect( e ).to.be.eql( testString1 ) 467 | } 468 | } ) 469 | 470 | describe( "Promise", () => { 471 | describe( "resolved", () => { 472 | 473 | it( "before", async () => { 474 | try { 475 | const value = await Failure( testString2 ) 476 | const pValue = await Promise.resolve( testString1 ) 477 | throw "The line should not be reached" 478 | } catch ( e ) { 479 | expect( e ).to.be.eql( testString2 ) 480 | } 481 | } ) 482 | 483 | it( "after", async () => { 484 | try { 485 | const pValue = await Promise.resolve( testString1 ) 486 | const value = await Failure( testString2 ) 487 | throw "The line should not be reached" 488 | } catch ( e ) { 489 | expect( e ).to.be.eql( testString2 ) 490 | } 491 | } ) 492 | } ) 493 | describe( "rejected", () => { 494 | 495 | it( "before", async () => { 496 | try { 497 | const pValue = await Promise.reject( testString1 ) 498 | const value = await Failure( testString2 ) 499 | throw "The line should not be reached" 500 | } catch ( e ) { 501 | expect( e ).to.be.eql( testString1 ) 502 | } 503 | } ) 504 | 505 | it( "after", async () => { 506 | try { 507 | const value = await Failure( testString1 ) 508 | const pValue = await Promise.reject( testString1 ) 509 | throw "The line should not be reached" 510 | } catch ( e ) { 511 | expect( e ).to.be.eql( testString1 ) 512 | } 513 | } ) 514 | } ) 515 | } ) 516 | } ) 517 | } ) 518 | } ) -------------------------------------------------------------------------------- /src/result.ts: -------------------------------------------------------------------------------- 1 | import { CJustSuccess, CNoneFailure, fulfilled, rejected } from "./monad" 2 | import { Thenable } from './thenable' 3 | 4 | const thenErrorMsg = "Result.then() is should be full filled by monad decorator." 5 | 6 | /** 7 | * Values which might represent an Error 8 | */ 9 | export type Throwable = Error | string 10 | 11 | /** 12 | * Define properties specific to Result monad 13 | * 14 | * @note then() is not defined, since it is attached by decorator 15 | */ 16 | interface IResult extends Thenable { 17 | /** 18 | * Implementation of PromiseLike.then() for proper functioning of await 19 | * @param onfulfilled Handler for fulfilled value 20 | * @param onrejected Handler for onrejected value 21 | * @return PromiseLike object which inclose new value 22 | */ 23 | then( 24 | onfulfilled?: ( ( value: T ) => TResult1 | PromiseLike ) | undefined | null, 25 | onrejected?: ( ( reason: any ) => TResult2 | PromiseLike ) | undefined | null 26 | ): PromiseLike 27 | /** 28 | * Accordingly apply the handlers produces a new Result as container for produced output 29 | * @param onJust Handler for fulfilled value 30 | * @param onNone Handler for onrejected value 31 | * @return Result object which inclose new value 32 | */ 33 | then( 34 | onSuccess?: ( value: T ) => TResult1 | IResult, 35 | onFailure?: ( reason: E ) => EResult2 | IResult 36 | ): Result 37 | /** 38 | * @returns Wether this is Failure 39 | */ 40 | isFailure(): this is Failure 41 | /** 42 | * @returns Wether this is Success 43 | */ 44 | isSuccess(): this is Success 45 | } 46 | 47 | /** 48 | * Represents a result of successful computation 49 | */ 50 | export type Success = CSuccess 51 | 52 | /** 53 | * Represents an Error occurred in during execution 54 | */ 55 | export type Failure = CFailure 56 | 57 | /** 58 | * Represents a result of computation which can potentially fail 59 | */ 60 | export type Result = Success | Failure 61 | 62 | /** 63 | * @param value Value which is going to be inclosed inside of Result 64 | * @returns Result with inclosed value as Success 65 | */ 66 | export const Success = 67 | ( value: T ): Result => 68 | new CSuccess( value ) as any 69 | 70 | /** 71 | * @param error Error which is going to be inclosed inside of Result 72 | * @returns Result with inclosed error as Failure 73 | */ 74 | export const Failure = 75 | ( error: E ): Result => 76 | new CFailure( error ) as any 77 | 78 | /** 79 | * @param action Function for evaluation 80 | * @returns Result of the evaluated function as 81 | * Success or Failure depending from situation 82 | */ 83 | export const Result = ( 84 | action: () => T | Result 85 | ) => { 86 | try { 87 | const result = action() 88 | if ( isResult( result ) ) { 89 | return result 90 | } else { 91 | return Success( result ) 92 | } 93 | } catch ( e ) { 94 | return Failure( e ) 95 | } 96 | } 97 | 98 | /** 99 | * @param obj Object which might be Result 100 | * @returns Wether the object is Result 101 | */ 102 | export const isResult = 103 | ( obj: any ): obj is Result => 104 | isSuccess( obj ) || isFailure( obj ) 105 | 106 | /** 107 | * @param obj Object which might be Failure 108 | * @returns Wether the object is Failure 109 | */ 110 | export const isFailure = 111 | ( obj: any ): obj is Failure => 112 | obj instanceof CFailure 113 | 114 | /** 115 | * @param obj Object which might be Success 116 | * @returns Wether the object is Success 117 | */ 118 | export const isSuccess = 119 | ( obj: any ): obj is Success => 120 | obj instanceof CSuccess 121 | 122 | /** 123 | * Container which represents result of successful computation 124 | */ 125 | class CSuccess extends CJustSuccess implements IResult { 126 | 127 | /** 128 | * Implementation of PromiseLike.then() for proper functioning of await 129 | * @param onfulfilled Handler for fulfilled value 130 | * @param onrejected Handler for onrejected value 131 | * @return PromiseLike object which inclose new value 132 | */ 133 | @fulfilled( isResult ) 134 | then( 135 | onSuccess?: ( value: T ) => TResult1 | IResult, 136 | onFailure?: ( reason: E ) => EResult2 | IResult 137 | ): Result { 138 | throw new Error( thenErrorMsg ) 139 | } 140 | 141 | isFailure(): this is Failure { 142 | return false 143 | } 144 | 145 | isSuccess(): this is Success { 146 | return true 147 | } 148 | } 149 | 150 | /** 151 | * Container which represents an Error occurred in during execution 152 | */ 153 | class CFailure extends CNoneFailure implements IResult { 154 | 155 | /** 156 | * Implementation of PromiseLike.then() for proper functioning of await 157 | * @param onfulfilled Handler for fulfilled value 158 | * @param onrejected Handler for onrejected value 159 | * @return PromiseLike object which inclose new value 160 | */ 161 | @rejected( isResult ) 162 | then( 163 | onSuccess?: ( value: T ) => TResult1 | IResult, 164 | onFailure?: ( reason: E ) => EResult2 | IResult 165 | ): Result { 166 | throw new Error( thenErrorMsg ) 167 | } 168 | 169 | isFailure(): this is Failure { 170 | return true 171 | } 172 | 173 | isSuccess(): this is Success { 174 | return false 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/testUtils.ts: -------------------------------------------------------------------------------- 1 | export const testNumber1 = 1 2 | export const testNumber2 = 2 3 | 4 | export const testString1 = "test1" 5 | export const testString2 = "test2" 6 | 7 | export const mapNumbersToNumber = ( v: number ) => v + testNumber2 8 | export const mapNumberToString = ( v: number ) => v + testString1 9 | export const mapStringToString = ( v: string ) => v + testString2 10 | -------------------------------------------------------------------------------- /src/thenable.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | 3 | import { isThenable } from "./thenable" 4 | import { Just, None } from './maybe' 5 | import { Success, Failure } from './result' 6 | 7 | describe( "Thenable", () => { 8 | describe( "is", () => { 9 | const isTrue = ( obj: any ) => expect( obj ).to.be.true 10 | 11 | it( "Promise", () => { 12 | isTrue( isThenable( Promise.resolve() ) ) 13 | } ) 14 | 15 | it( "Just", () => { 16 | isTrue( isThenable( Just( "" ) ) ) 17 | } ) 18 | 19 | it( "None", () => { 20 | isTrue( isThenable( None() ) ) 21 | } ) 22 | 23 | it( "Success", () => { 24 | isTrue( isThenable( Success( "" ) ) ) 25 | } ) 26 | 27 | it( "Failure", () => { 28 | isTrue( isThenable( Failure( "" ) ) ) 29 | } ) 30 | 31 | it( "any.then()", () => { 32 | isTrue( isThenable( { then: () => { } } ) ) 33 | } ) 34 | } ) 35 | 36 | describe( "isn't", () => { 37 | const isFalse = ( obj: any ) => expect( obj ).to.be.false 38 | 39 | it( "object", () => { 40 | isFalse( isThenable( {} ) ) 41 | } ) 42 | 43 | it( "number", () => { 44 | isFalse( isThenable( 10 ) ) 45 | } ) 46 | 47 | it( "boolean", () => { 48 | isFalse( isThenable( true ) ) 49 | } ) 50 | 51 | it( "string", () => { 52 | isFalse( isThenable( "true" ) ) 53 | } ) 54 | 55 | it( "any.then === {}", () => { 56 | isFalse( isThenable( { then: {} } ) ) 57 | } ) 58 | } ) 59 | } ) -------------------------------------------------------------------------------- /src/thenable.ts: -------------------------------------------------------------------------------- 1 | export interface Thenable extends PromiseLike { 2 | /** 3 | * Implementation of PromiseLike.then() for proper functioning of await 4 | * @param onfulfilled Handler for fulfilled value 5 | * @param onrejected Handler for onrejected value 6 | * @return PromiseLike object which inclose new value 7 | */ 8 | then( 9 | onfulfilled?: ( ( value: T ) => TResult1 | PromiseLike ) | undefined | null, 10 | onrejected?: ( ( reason: any ) => TResult2 | PromiseLike ) | undefined | null 11 | ): PromiseLike 12 | } 13 | 14 | /** 15 | * @param obj Object which might be Thenable 16 | * @returns Wether the object is Thenable 17 | */ 18 | export const isThenable = ( obj: any ): obj is Thenable => typeof obj.then === 'function' -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "sourceMap": true, 8 | "strict": true, 9 | "noImplicitAny": false, 10 | "removeComments": true, 11 | "preserveConstEnums": true, 12 | "noEmitHelpers": false, 13 | "outDir": "dist", 14 | "rootDir": "src", 15 | "pretty": true, 16 | "declaration": true, 17 | "importHelpers": false 18 | }, 19 | "compileOnSave": true, 20 | "buildOnSave": true, 21 | "files": [ 22 | "./src/index.ts" 23 | ], 24 | "exclude": [ 25 | "**/*.spec.ts", 26 | "dist", 27 | "node_modules" 28 | ] 29 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": false, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-var-keyword": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | false, 24 | "double" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "never" 29 | ], 30 | "triple-equals": [ 31 | true, 32 | "allow-null-check" 33 | ], 34 | "typedef-whitespace": [ 35 | true, 36 | { 37 | "call-signature": "nospace", 38 | "index-signature": "nospace", 39 | "parameter": "nospace", 40 | "property-declaration": "nospace", 41 | "variable-declaration": "nospace" 42 | } 43 | ], 44 | "variable-name": [ 45 | true, 46 | "ban-keywords" 47 | ], 48 | "whitespace": [ 49 | true, 50 | "check-branch", 51 | "check-decl", 52 | "check-operator", 53 | "check-separator", 54 | "check-type" 55 | ] 56 | } 57 | } --------------------------------------------------------------------------------