├── .gitignore ├── src ├── tslint.json ├── monapt.ts ├── tsconfig.json ├── try │ ├── try.ts │ ├── success.ts │ └── failure.ts ├── option │ ├── some.ts │ ├── none.ts │ └── option.ts └── future │ └── future.ts ├── test ├── tslint.json ├── tsconfig.json ├── try │ ├── try.ts │ ├── success.ts │ └── failure.ts ├── option │ ├── option.ts │ ├── none.ts │ └── some.ts └── future │ └── future.ts ├── test.webpack.config.js ├── LICENSE.md ├── circle.yml ├── package.json ├── README.md └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /dist 3 | /node_modules 4 | /test/dist 5 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | // We're using ES5. 5 | "promise-function-async": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/monapt.ts: -------------------------------------------------------------------------------- 1 | export { Failure } from './try/failure'; 2 | export { Future } from './future/future'; 3 | export { None } from './option/none'; 4 | export { Option } from './option/option'; 5 | export { Some } from './option/some'; 6 | export { Success } from './try/success'; 7 | export { Try } from './try/try'; 8 | -------------------------------------------------------------------------------- /test/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | // AVA uses ES6. 5 | "no-floating-promises": true, 6 | "no-magic-numbers": false, 7 | // AVA has a lot of functions that take in a type 'any'. 8 | "no-unsafe-any": false, 9 | "promise-function-async": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "module": "commonjs", 5 | "target": "es5", 6 | "noImplicitAny": true, 7 | "sourceMap": true, 8 | "experimentalDecorators": true, 9 | "noEmitOnError": true, 10 | "noUnusedLocals": true, 11 | "lib": ["es2015.promise", "es5", "dom"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | // AVA requires us to support es2015 in order to use promises and async functions. 5 | // :LINK: https://github.com/avajs/ava/tree/c01ac05ef610c87f5c264b1a3eb80f89b4fc0832#typescript-support 6 | "target": "es2015", 7 | "noImplicitAny": true, 8 | // nyc/istanbul requires source maps in order to do line mappings. 9 | "sourceMap": true, 10 | "experimentalDecorators": true, 11 | "noEmitOnError": true, 12 | "noUnusedLocals": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/try/try.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { Failure } from '../../src/try/failure'; 4 | import { Success } from '../../src/try/success'; 5 | import { Try } from '../../src/try/try'; 6 | 7 | test('Try::apply', (t: ExecutionContext) => { 8 | t.deepEqual( 9 | Try(() => 'hello'), 10 | new Success('hello') 11 | ); 12 | 13 | t.deepEqual( 14 | Try((): void => { throw new Error('hello'); }), 15 | new Failure(new Error('hello')) 16 | ); 17 | }); 18 | 19 | test('Try::flatten', (t: ExecutionContext) => { 20 | t.deepEqual( 21 | Try.flatten([ 22 | new Failure(new Error()), 23 | new Success(1), 24 | new Success(2), 25 | new Failure(new Error()), 26 | new Success(3) 27 | ]), 28 | [1, 2, 3] 29 | ); 30 | }); 31 | -------------------------------------------------------------------------------- /test/option/option.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { None } from '../../src/option/none'; 4 | import { Option } from '../../src/option/option'; 5 | import { Some } from '../../src/option/some'; 6 | 7 | test('Option::apply', (t: ExecutionContext) => { 8 | t.deepEqual( 9 | Option('hello'), 10 | new Some('hello') 11 | ); 12 | 13 | /* tslint:disable:no-null-keyword */ 14 | t.deepEqual( 15 | Option(null), 16 | None 17 | ); 18 | /* tslint:enable */ 19 | 20 | t.deepEqual( 21 | Option(undefined), 22 | None 23 | ); 24 | }); 25 | 26 | test('Option::flatten', (t: ExecutionContext) => { 27 | t.deepEqual( 28 | Option.flatten([ 29 | None, 30 | new Some(1), 31 | new Some(2), 32 | None, 33 | new Some(3) 34 | ]), 35 | [1, 2, 3] 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /test.webpack.config.js: -------------------------------------------------------------------------------- 1 | const glob = require('glob'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: glob.sync('./test/**/*.ts', { ignore: glob.sync('./test/dist/**/*.ts') }), 6 | output: { 7 | filename: '[name].js', 8 | libraryTarget: 'commonjs', 9 | path: path.resolve(__dirname, 'test/dist') 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.ts$/, 15 | rules: [ 16 | { 17 | loader: 'ts-loader' 18 | }, 19 | { 20 | exclude: /test\/.*\.ts$/, 21 | enforce: 'post', 22 | loader: 'istanbul-instrumenter-loader', 23 | } 24 | ] 25 | } 26 | ] 27 | }, 28 | // nyc/istanbul requires inline source maps in order to do line mappings. 29 | devtool: 'inline-source-map', 30 | resolve: { 31 | modules: [ 32 | 'node_modules' 33 | ], 34 | extensions: ['.ts', '.js'] 35 | }, 36 | externals: ['ava'] 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kevin Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/monapt 5 | docker: 6 | - image: node:boron 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: node-{{ .Branch }}-{{ checksum "package.json" }} 11 | - run: 12 | name: Installing JS dependencies 13 | command: yarn 14 | - save_cache: 15 | key: node-{{ .Branch }}-{{ checksum "package.json" }} 16 | paths: 17 | - "node_modules" 18 | - run: 19 | name: Validating commit messages 20 | command: yarn commitplease origin/master..HEAD 21 | - run: 22 | name: Linting code 23 | command: yarn lint:ts 24 | - run: 25 | name: Running tests 26 | command: yarn test 27 | - run: 28 | name: Reporting code coverage to Coveralls 29 | command: yarn coverage 30 | - run: 31 | name: Publishing to npm (master only) 32 | command: | 33 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 34 | yarn build 35 | yarn semantic-release || true 36 | fi 37 | -------------------------------------------------------------------------------- /src/try/try.ts: -------------------------------------------------------------------------------- 1 | import { Failure } from './failure'; 2 | import { Option } from '../option/option'; 3 | import { Success } from './success'; 4 | 5 | /** 6 | * A Try represents a computation that may or may not have succeeded. 7 | * 8 | * :TODO: Narrow self-type in `getOrElse` and `orElse` interfaces. 9 | */ 10 | interface Try { 11 | isFailure: boolean; 12 | isSuccess: boolean; 13 | equals(other: Try): boolean; 14 | filter(predicate: (value: A) => boolean): Try; 15 | flatMap(f: (value: A) => Try): Try; 16 | foreach(run: (value: A) => void): void; 17 | get(): A; 18 | getOrElse(defaultValue: () => B): B; 19 | map(f: (value: A) => B): Try; 20 | match(matcher: { Success: (a: A) => B, Failure: (e: Error) => B }): B; 21 | orElse(alternative: () => Try): Try; 22 | recover(fn: (error: Error) => A): Try; 23 | recoverWith(fn: (error: Error) => Try): Try; 24 | toOption(): Option; 25 | } 26 | 27 | /* tslint:disable:no-null-keyword only-arrow-functions */ 28 | function Try(fn: () => A): Try { 29 | try { 30 | return new Success(fn()); 31 | } 32 | catch (e) { 33 | return new Failure(e as Error); 34 | } 35 | } 36 | /* tslint:enable */ 37 | 38 | /* tslint:disable:no-namespace */ 39 | namespace Try { 40 | /* tslint:disable:only-arrow-functions */ 41 | export function flatten(tries: Array>): Array { 42 | return tries 43 | .filter((attempt: Try): boolean => attempt.isSuccess) 44 | .map((attempt: Try): A => attempt.get()); 45 | } 46 | /* tslint:enable */ 47 | } 48 | /* tslint:enable */ 49 | 50 | export { Try }; 51 | -------------------------------------------------------------------------------- /src/option/some.ts: -------------------------------------------------------------------------------- 1 | import { None } from './none'; 2 | import { Option } from './option'; 3 | 4 | /** 5 | * Some represents non-emptiness. It signals that a value that may not exist does actually exist. 6 | */ 7 | class Some implements Option { 8 | private value: A; 9 | 10 | get isDefined(): boolean { 11 | return true; 12 | } 13 | 14 | get isEmpty(): boolean { 15 | return false; 16 | } 17 | 18 | constructor(value: A) { 19 | this.value = value; 20 | } 21 | 22 | equals(other: Option): boolean { 23 | return (other instanceof Some) && this.get() === other.get(); 24 | } 25 | 26 | filter(predicate: (value: A) => boolean): Option { 27 | if (predicate(this.value)) { 28 | return this; 29 | } 30 | else { 31 | return None; 32 | } 33 | } 34 | 35 | filterNot(predicate: (value: A) => boolean): Option { 36 | if (!predicate(this.value)) { 37 | return this; 38 | } 39 | else { 40 | return None; 41 | } 42 | } 43 | 44 | flatMap(flatMapper: (value: A) => Option): Option { 45 | return flatMapper(this.value); 46 | } 47 | 48 | foreach(run: (value: A) => void): void { 49 | run(this.value); 50 | } 51 | 52 | get(): A { 53 | return this.value; 54 | } 55 | 56 | getOrElse(this: Some, defaultValue: () => B): B { 57 | return this.value; 58 | } 59 | 60 | map(mapper: (value: A) => B): Option { 61 | return new Some(mapper(this.value)); 62 | } 63 | 64 | match(matcher: { Some: (a: A) => B, None: () => B }): B { 65 | return matcher.Some(this.value); 66 | } 67 | 68 | orElse(this: Some, alternative: () => Option): Option { 69 | return this; 70 | } 71 | } 72 | 73 | export { Some }; 74 | -------------------------------------------------------------------------------- /src/option/none.ts: -------------------------------------------------------------------------------- 1 | import { Option } from './option'; 2 | 3 | /** 4 | * None is a singleton class that represents emptiness. It signals that a value that may not exist 5 | * doesn't exist. 6 | */ 7 | /* tslint:disable:class-name */ 8 | class None_ implements Option { 9 | /* tslint:enable:class-name */ 10 | static get INSTANCE(): None_ { 11 | return new None_(); 12 | } 13 | 14 | get isDefined(): boolean { 15 | return false; 16 | } 17 | 18 | get isEmpty(): boolean { 19 | return true; 20 | } 21 | 22 | private constructor() { 23 | // :TRICKY: We don't want to do anything except mark constructor as private. 24 | } 25 | 26 | equals(other: Option): boolean { 27 | return (other instanceof None_); 28 | } 29 | 30 | filter(predicate: (value: A) => boolean): Option { 31 | return this; 32 | } 33 | 34 | filterNot(predicate: (value: A) => boolean): Option { 35 | return this; 36 | } 37 | 38 | flatMap(flatMapper: (value: A) => Option): Option { 39 | return None_.INSTANCE; 40 | } 41 | 42 | foreach(run: (value: A) => void): void { 43 | // :TRICKY: Don't do anything. 44 | } 45 | 46 | get(): A { 47 | throw new Option.NoSuchElementError(); 48 | } 49 | 50 | getOrElse(this: None_, defaultValue: () => B): B { 51 | return defaultValue(); 52 | } 53 | 54 | map(mapper: (value: A) => B): Option { 55 | return None_.INSTANCE; 56 | } 57 | 58 | match(matcher: { Some: (a: A) => B, None: () => B }): B { 59 | return matcher.None(); 60 | } 61 | 62 | orElse(this: None_, alternative: () => Option): Option { 63 | return alternative(); 64 | } 65 | } 66 | 67 | const instance: None_ = None_.INSTANCE; 68 | 69 | export { None_, instance as None }; 70 | -------------------------------------------------------------------------------- /src/try/success.ts: -------------------------------------------------------------------------------- 1 | import { Failure } from './failure'; 2 | import { Option } from '../option/option'; 3 | import { Try } from './try'; 4 | 5 | /** 6 | * Success represents a computation that ended normally. It signals that a computation that may or 7 | * may not have completed did actually complete. 8 | */ 9 | class Success implements Try { 10 | private value: A; 11 | 12 | get isFailure(): boolean { 13 | return false; 14 | } 15 | 16 | get isSuccess(): boolean { 17 | return true; 18 | } 19 | 20 | constructor(value: A) { 21 | this.value = value; 22 | } 23 | 24 | equals(other: Try): boolean { 25 | return (other instanceof Success) && this.value === other.value; 26 | } 27 | 28 | filter(predicate: (value: A) => boolean): Try { 29 | if (predicate(this.value)) { 30 | return this; 31 | } 32 | else { 33 | return new Failure(new Error(`Predicate does not hold for ${this.value}`)); 34 | } 35 | } 36 | 37 | flatMap(f: (value: A) => Try): Try { 38 | return f(this.value); 39 | } 40 | 41 | foreach(run: (value: A) => void): void { 42 | run(this.value); 43 | } 44 | 45 | get(): A { 46 | return this.value; 47 | } 48 | 49 | getOrElse(this: Success, defaultValue: () => B): B { 50 | return this.value; 51 | } 52 | 53 | map(f: (value: A) => B): Try { 54 | return new Success(f(this.value)); 55 | } 56 | 57 | match(matcher: { Success: (a: A) => B, Failure: (e: Error) => B }): B { 58 | return matcher.Success(this.value); 59 | } 60 | 61 | orElse(this: Success, alternative: () => Try): Try { 62 | return this; 63 | } 64 | 65 | recover(fn: (error: Error) => A): Try { 66 | return this; 67 | } 68 | 69 | recoverWith(fn: (error: Error) => Try): Try { 70 | return this; 71 | } 72 | 73 | toOption(): Option { 74 | return Option(this.value); 75 | } 76 | } 77 | 78 | export { Success }; 79 | -------------------------------------------------------------------------------- /src/try/failure.ts: -------------------------------------------------------------------------------- 1 | import { None } from '../option/none'; 2 | import { Option } from '../option/option'; 3 | import { Try } from './try'; 4 | 5 | /** 6 | * Failure represents a computation that threw an exception during execution. It signals that a 7 | * computation that may or may not have succeeded ended up failing. 8 | */ 9 | class Failure implements Try { 10 | private error: Error; 11 | 12 | get exception(): Error { 13 | return this.error; 14 | } 15 | 16 | get isFailure(): boolean { 17 | return true; 18 | } 19 | 20 | get isSuccess(): boolean { 21 | return false; 22 | } 23 | 24 | constructor(error: Error) { 25 | this.error = error; 26 | } 27 | 28 | equals(other: Try): boolean { 29 | return (other instanceof Failure) && this.error === other.error; 30 | } 31 | 32 | filter(predicate: (value: A) => boolean): Try { 33 | return this; 34 | } 35 | 36 | flatMap(f: (value: A) => Try): Try { 37 | return new Failure(this.error); 38 | } 39 | 40 | foreach(run: (value: A) => void): void { 41 | // :TRICKY: Don't run it, since it's a failure. 42 | } 43 | 44 | get(): A { 45 | throw this.error; 46 | } 47 | 48 | getOrElse(this: Failure, defaultValue: () => B): B { 49 | return defaultValue(); 50 | } 51 | 52 | map(f: (value: A) => B): Try { 53 | return new Failure(this.error); 54 | } 55 | 56 | match(matcher: { Success: (a: A) => B, Failure: (e: Error) => B }): B { 57 | return matcher.Failure(this.error); 58 | } 59 | 60 | orElse(this: Failure, alternative: () => Try): Try { 61 | return alternative(); 62 | } 63 | 64 | recover(fn: (error: Error) => A): Try { 65 | return Try(() => fn(this.error)); 66 | } 67 | 68 | recoverWith(fn: (error: Error) => Try): Try { 69 | return fn(this.error); 70 | } 71 | 72 | toOption(): Option { 73 | return None; 74 | } 75 | } 76 | 77 | export { Failure }; 78 | -------------------------------------------------------------------------------- /src/option/option.ts: -------------------------------------------------------------------------------- 1 | import { None } from './none'; 2 | import { Some } from './some'; 3 | 4 | /** 5 | * An Option represents a collection of size 0 or 1. Traditionally it's used as a replacement for 6 | * `null` or `undefined` to explicitly encode if a value can be empty or not. 7 | * 8 | * :TODO: Narrow self-type in `getOrElse` and `orElse` interfaces. 9 | */ 10 | interface Option { 11 | isDefined: boolean; 12 | isEmpty: boolean; 13 | 14 | equals(other: Option): boolean; 15 | filter(predicate: (value: A) => boolean): Option; 16 | filterNot(predicate: (value: A) => boolean): Option; 17 | flatMap(flatMapper: (value: A) => Option): Option; 18 | foreach(run: (value: A) => void): void; 19 | get(): A; 20 | getOrElse(defaultValue: () => B): B; 21 | map(mapper: (value: A) => B): Option; 22 | match(matcher: { Some: (a: A) => B, None: () => B }): B; 23 | orElse(alternative: () => Option): Option; 24 | } 25 | 26 | /* tslint:disable:no-null-keyword only-arrow-functions */ 27 | function Option(value: A | null | undefined): Option { 28 | if ((value === null) || (value === undefined)) { 29 | return None; 30 | } 31 | else { 32 | return new Some(value); 33 | } 34 | } 35 | /* tslint:enable */ 36 | 37 | /* tslint:disable:no-namespace */ 38 | namespace Option { 39 | /** 40 | * This error is thrown when `None.get()` is called. 41 | */ 42 | export class NoSuchElementError extends Error { 43 | constructor() { 44 | super(); 45 | 46 | this.name = 'NoSuchElementError'; 47 | this.message = 'Monapt.Option.get(): No such element.'; 48 | this.stack = (new Error()).stack; 49 | } 50 | } 51 | 52 | /* tslint:disable:only-arrow-functions */ 53 | export function flatten(options: Array>): Array { 54 | return options 55 | .filter((option: Option): boolean => option.isDefined) 56 | .map((option: Some): A => option.get()); 57 | } 58 | /* tslint:enable */ 59 | } 60 | /* tslint:enable */ 61 | 62 | export { Option }; 63 | -------------------------------------------------------------------------------- /test/option/none.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { None } from '../../src/option/none'; 4 | import { Option } from '../../src/option/option'; 5 | 6 | test('None#isDefined', (t: ExecutionContext) => { 7 | t.false(None.isDefined); 8 | }); 9 | 10 | test('None#isEmpty', (t: ExecutionContext) => { 11 | t.true(None.isEmpty); 12 | }); 13 | 14 | test('None#equals', (t: ExecutionContext) => { 15 | t.true(None.equals(None)); 16 | t.false(None.equals(Option(1))); 17 | }); 18 | 19 | test('None#filter', (t: ExecutionContext) => { 20 | t.is( 21 | None.filter(() => true), 22 | None 23 | ); 24 | t.is( 25 | None.filter(() => false), 26 | None 27 | ); 28 | }); 29 | 30 | test('None#filterNot', (t: ExecutionContext) => { 31 | t.is( 32 | None.filterNot(() => true), 33 | None 34 | ); 35 | t.is( 36 | None.filterNot(() => false), 37 | None 38 | ); 39 | }); 40 | 41 | test('None#flatMap', (t: ExecutionContext) => { 42 | t.deepEqual( 43 | None.flatMap(() => Option('hello')), 44 | None 45 | ); 46 | }); 47 | 48 | test('None#foreach', (t: ExecutionContext) => { 49 | let executions: number = 0; 50 | 51 | None.foreach(() => { executions += 1; }); 52 | 53 | t.is(executions, 0); 54 | }); 55 | 56 | test('None#get', (t: ExecutionContext) => { 57 | t.throws((): never => None.get(), { instanceOf: Option.NoSuchElementError }); 58 | }); 59 | 60 | test('None#getOrElse', (t: ExecutionContext) => { 61 | t.is( 62 | None.getOrElse(() => 'hello'), 63 | 'hello' 64 | ); 65 | }); 66 | 67 | test('None#map', (t: ExecutionContext) => { 68 | t.deepEqual( 69 | None.map((v: never) => 'hello'), 70 | None 71 | ); 72 | }); 73 | 74 | test('None#match', (t: ExecutionContext) => { 75 | t.is( 76 | None.match({ 77 | Some: (): string => 'hello', 78 | None: (): string => 'world' 79 | }), 80 | 'world' 81 | ); 82 | }); 83 | 84 | test('None#orElse', (t: ExecutionContext) => { 85 | t.deepEqual( 86 | None.orElse(() => Option('hello')), 87 | Option('hello') 88 | ); 89 | }); 90 | -------------------------------------------------------------------------------- /test/option/some.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { None } from '../../src/option/none'; 4 | import { Option } from '../../src/option/option'; 5 | 6 | const some: Option = Option('hello'); 7 | 8 | test('Some#isDefined', (t: ExecutionContext) => { 9 | t.true(some.isDefined); 10 | }); 11 | 12 | test('Some#isEmpty', (t: ExecutionContext) => { 13 | t.false(some.isEmpty); 14 | }); 15 | 16 | test('Some#equals', (t: ExecutionContext) => { 17 | t.true(some.equals(Option('hello'))); 18 | t.false(some.equals(Option(1))); 19 | t.false(some.equals(None)); 20 | }); 21 | 22 | test('Some#filter', (t: ExecutionContext) => { 23 | t.is( 24 | some.filter((v: string) => v === 'hello'), 25 | some 26 | ); 27 | t.is( 28 | some.filter((v: string) => v === 'world'), 29 | None 30 | ); 31 | }); 32 | 33 | test('Some#filterNot', (t: ExecutionContext) => { 34 | t.is( 35 | some.filterNot((v: string) => v === 'hello'), 36 | None 37 | ); 38 | t.is( 39 | some.filterNot((v: string) => v === 'world'), 40 | some 41 | ); 42 | }); 43 | 44 | test('Some#flatMap', (t: ExecutionContext) => { 45 | t.deepEqual( 46 | some.flatMap(() => Option('world')), 47 | Option('world') 48 | ); 49 | }); 50 | 51 | test('Some#foreach', (t: ExecutionContext) => { 52 | let executions: number = 0; 53 | 54 | some.foreach(() => { executions += 1; }); 55 | 56 | t.is(executions, 1); 57 | }); 58 | 59 | test('Some#get', (t: ExecutionContext) => { 60 | try { 61 | t.is( 62 | some.get(), 63 | 'hello' 64 | ); 65 | } 66 | catch (e) { 67 | t.fail(); 68 | } 69 | }); 70 | 71 | test('Some#getOrElse', (t: ExecutionContext) => { 72 | t.is( 73 | some.getOrElse(() => 'world'), 74 | 'hello' 75 | ); 76 | }); 77 | 78 | test('Some#map', (t: ExecutionContext) => { 79 | t.deepEqual( 80 | some.map((v: string) => `${v} world`), 81 | Option('hello world') 82 | ); 83 | }); 84 | 85 | test('Some#match', (t: ExecutionContext) => { 86 | t.is( 87 | some.match({ 88 | Some: (v: string): string => v, 89 | None: (): string => 'world' 90 | }), 91 | 'hello' 92 | ); 93 | }); 94 | 95 | test('Some#orElse', (t: ExecutionContext) => { 96 | t.deepEqual( 97 | some.orElse(() => Option('world')), 98 | Option('hello') 99 | ); 100 | }); 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monapt", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Eliminate null/undefined errors in JS...and more!", 5 | "author": { 6 | "name": "Kevin Li", 7 | "email": "jiawei.h.li+npm@gmail.com", 8 | "url": "https://jiawei.li/" 9 | }, 10 | "license": "MIT", 11 | "homepage": "https://github.com/jiaweihli/monapt#readme", 12 | "bugs": { 13 | "url": "https://github.com/jiaweihli/monapt/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/jiaweihli/monapt.git" 18 | }, 19 | "scripts": { 20 | "build": "webpack --config build.webpack.config.js", 21 | "commit": "git-cz", 22 | "commitmsg": "commitplease .git/COMMIT_EDITMSG", 23 | "coverage": "nyc report --reporter=text-lcov | coveralls", 24 | "lint:ts": "tslint --format verbose --config ./src/tslint.json --project ./src/tsconfig.json --type-check './src/**/*.ts' && tslint --format verbose --config ./test/tslint.json --project ./test/tsconfig.json --type-check './test/**/*.ts'", 25 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 26 | "smoke:ts": "tsc --noEmit --pretty --project ./src/tsconfig.json && tsc --noEmit --pretty --project ./test/tsconfig.json", 27 | "test": "webpack --config test.webpack.config.js && nyc ava --verbose", 28 | "watch": "webpack --config build.webpack.config.js --watch" 29 | }, 30 | "main": "dist/monapt.js", 31 | "typings": "dist/src/monapt.d.ts", 32 | "files": [ 33 | "dist/", 34 | "README.md" 35 | ], 36 | "keywords": [ 37 | "option", 38 | "optional", 39 | "future", 40 | "try", 41 | "monad", 42 | "functional", 43 | "util", 44 | "scala", 45 | "type", 46 | "types" 47 | ], 48 | "devDependencies": { 49 | "ava": "1.0.0-beta.3", 50 | "commitizen": "2.9.6", 51 | "commitplease": "2.7.10", 52 | "condition-circle": "1.5.0", 53 | "conventional-changelog-cli": "1.3.1", 54 | "conventional-commit-types": "2.1.0", 55 | "coveralls": "2.13.1", 56 | "cracks": "3.1.2", 57 | "cz-conventional-changelog": "2.0.0", 58 | "glob": "7.1.2", 59 | "husky": "0.14.1", 60 | "istanbul-instrumenter-loader": "2.0.0", 61 | "nyc": "11.0.3", 62 | "semantic-release": "6.3.6", 63 | "ts-loader": "2.2.1", 64 | "tslint": "5.1.0", 65 | "typescript": "2.7.1", 66 | "webpack": "2.6.1" 67 | }, 68 | "commitplease": { 69 | "nohook": true, 70 | "style": "angular", 71 | "types": [ 72 | "build", 73 | "chore", 74 | "ci", 75 | "docs", 76 | "feat", 77 | "fix", 78 | "perf", 79 | "refactor", 80 | "style", 81 | "test" 82 | ] 83 | }, 84 | "config": { 85 | "commitizen": { 86 | "path": "./node_modules/cz-conventional-changelog" 87 | } 88 | }, 89 | "release": { 90 | "verifyConditions": "condition-circle" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /test/try/success.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { Failure } from '../../src/try/failure'; 4 | import { Some } from '../../src/option/some'; 5 | import { Success } from '../../src/try/success'; 6 | import { Try } from '../../src/try/try'; 7 | 8 | const success: Try = new Success('hello'); 9 | 10 | test('Success#isFailure', (t: ExecutionContext) => { 11 | t.false(success.isFailure); 12 | }); 13 | 14 | test('Success#isSuccess', (t: ExecutionContext) => { 15 | t.true(success.isSuccess); 16 | }); 17 | 18 | test('Success#equals', (t: ExecutionContext) => { 19 | t.true(success.equals(new Success('hello'))); 20 | 21 | t.false(success.equals(new Success('world'))); 22 | }); 23 | 24 | test('Success#filter', (t: ExecutionContext) => { 25 | t.is( 26 | success.filter((v: string) => v === 'hello'), 27 | success 28 | ); 29 | t.deepEqual( 30 | success.filter((v: string) => v === 'world'), 31 | new Failure(new Error('Predicate does not hold for hello')) 32 | ); 33 | }); 34 | 35 | test('Success#flatMap', (t: ExecutionContext) => { 36 | t.deepEqual( 37 | success.flatMap(() => new Success('world')), 38 | new Success('world') 39 | ); 40 | }); 41 | 42 | test('Success#foreach', (t: ExecutionContext) => { 43 | let executions: number = 0; 44 | 45 | success.foreach(() => { executions += 1; }); 46 | 47 | t.is(executions, 1); 48 | }); 49 | 50 | test('Success#get', (t: ExecutionContext) => { 51 | try { 52 | t.is( 53 | success.get(), 54 | 'hello' 55 | ); 56 | } 57 | catch (e) { 58 | t.fail(); 59 | } 60 | }); 61 | 62 | test('Success#getOrElse', (t: ExecutionContext) => { 63 | t.is( 64 | success.getOrElse(() => 'world'), 65 | 'hello' 66 | ); 67 | }); 68 | 69 | test('Success#map', (t: ExecutionContext) => { 70 | t.deepEqual( 71 | success.map((v: string) => `${v} world`), 72 | new Success('hello world') 73 | ); 74 | }); 75 | 76 | test('Success#match', (t: ExecutionContext) => { 77 | t.is( 78 | success.match({ 79 | Success: (v: string): string => v, 80 | Failure: (): string => 'world' 81 | }), 82 | 'hello' 83 | ); 84 | }); 85 | 86 | test('Success#orElse', (t: ExecutionContext) => { 87 | t.deepEqual( 88 | success.orElse(() => new Success('world')), 89 | new Success('hello') 90 | ); 91 | }); 92 | 93 | test('Success#recover', (t: ExecutionContext) => { 94 | t.is( 95 | success.recover((error: Error) => 'world'), 96 | success 97 | ); 98 | }); 99 | 100 | test('Success#recoverWith', (t: ExecutionContext) => { 101 | t.deepEqual( 102 | success.recoverWith((error: Error) => new Success('world')), 103 | success 104 | ); 105 | }); 106 | 107 | test('Success#toOption', (t: ExecutionContext) => { 108 | t.deepEqual( 109 | success.toOption(), 110 | new Some('hello') 111 | ); 112 | }); 113 | -------------------------------------------------------------------------------- /test/try/failure.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { Failure } from '../../src/try/failure'; 4 | import { None } from '../../src/option/none'; 5 | import { Success } from '../../src/try/success'; 6 | 7 | const errorMessage: string = 'test error message'; 8 | const error: Error = new Error(errorMessage); 9 | const failure: Failure = new Failure(error); 10 | 11 | test('Failure#exception', (t: ExecutionContext) => { 12 | t.is( 13 | failure.exception.message, 14 | errorMessage 15 | ); 16 | }); 17 | 18 | test('Failure#isFailure', (t: ExecutionContext) => { 19 | t.true(failure.isFailure); 20 | }); 21 | 22 | test('Failure#isSuccess', (t: ExecutionContext) => { 23 | t.false(failure.isSuccess); 24 | }); 25 | 26 | test('Failure#equals', (t: ExecutionContext) => { 27 | t.true(failure.equals(new Failure(error))); 28 | 29 | t.false(failure.equals(new Failure(new Error(errorMessage)))); 30 | }); 31 | 32 | test('Failure#filter', (t: ExecutionContext) => { 33 | t.is( 34 | failure.filter((v: String) => v === 'hello'), 35 | failure 36 | ); 37 | }); 38 | 39 | test('Failure#flatMap', (t: ExecutionContext) => { 40 | t.deepEqual( 41 | failure.flatMap(() => new Success('world')), 42 | failure 43 | ); 44 | }); 45 | 46 | test('Failure#foreach', (t: ExecutionContext) => { 47 | let executions: number = 0; 48 | 49 | failure.foreach(() => { executions += 1; }); 50 | 51 | t.is(executions, 0); 52 | }); 53 | 54 | test('Failure#get', (t: ExecutionContext) => { 55 | try { 56 | failure.get(); 57 | t.fail(); 58 | } 59 | catch (e) { 60 | t.pass(); 61 | } 62 | }); 63 | 64 | test('Failure#getOrElse', (t: ExecutionContext) => { 65 | t.is( 66 | failure.getOrElse(() => 'hello'), 67 | 'hello' 68 | ); 69 | }); 70 | 71 | test('Failure#map', (t: ExecutionContext) => { 72 | t.deepEqual( 73 | failure.map((v: string) => `${v} world`), 74 | failure 75 | ); 76 | }); 77 | 78 | test('Failure#match', (t: ExecutionContext) => { 79 | t.is( 80 | failure.match({ 81 | Success: (v: string): string => v, 82 | Failure: (): string => 'world' 83 | }), 84 | 'world' 85 | ); 86 | }); 87 | 88 | test('Failure#orElse', (t: ExecutionContext) => { 89 | t.deepEqual( 90 | failure.orElse(() => new Success('world')), 91 | new Success('world') 92 | ); 93 | }); 94 | 95 | test('Failure#recover', (t: ExecutionContext) => { 96 | t.deepEqual( 97 | failure.recover((error: Error) => 'world'), 98 | new Success('world') 99 | ); 100 | }); 101 | 102 | test('Failure#recoverWith', (t: ExecutionContext) => { 103 | t.deepEqual( 104 | failure.recoverWith((error: Error) => new Success('world')), 105 | new Success('world') 106 | ); 107 | }); 108 | 109 | test('Failure#toOption', (t: ExecutionContext) => { 110 | t.is( 111 | failure.toOption(), 112 | None 113 | ); 114 | }); 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monapt 2 | 3 | [![npm version](https://badge.fury.io/js/monapt.svg)](http://badge.fury.io/js/monapt) 4 | [![Build Status](https://circleci.com/gh/jiaweihli/monapt.svg?style=shield)](https://circleci.com/gh/jiaweihli/monapt) 5 | [![Coverage Status](https://coveralls.io/repos/github/jiaweihli/monapt/badge.svg)](https://coveralls.io/github/jiaweihli/monapt) 6 | 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 8 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 9 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-green.svg)](https://conventionalcommits.org) 10 | 11 | Monapt helps you better manage `null`, `undefined`, exceptions, and other mildly interesting 12 | phenomena. It handles them through the 13 | [`Option`](http://danielwestheide.com/blog/2012/12/19/the-neophytes-guide-to-scala-part-5-the-option-type.html), 14 | [`Try`](http://danielwestheide.com/blog/2012/12/26/the-neophytes-guide-to-scala-part-6-error-handling-with-try.html), 15 | and [`Future`](http://danielwestheide.com/blog/2013/01/09/the-neophytes-guide-to-scala-part-8-welcome-to-the-future.html) 16 | abstractions. 17 | 18 | 19 | ## Setup 20 | 21 | ```bash 22 | $ npm install monapt 23 | ``` 24 | 25 | ## APIs 26 | 27 | ### Usage 28 | 29 | ```typescript 30 | import { Option } from 'monapt'; 31 | 32 | Option(1) 33 | .map((x) => x * 2) 34 | .getOrElse(() => 4); 35 | ``` 36 | 37 | Docs are undergoing a redesign, and will be published on a separate site. 38 | In the meantime, the sources for the [`Option`](https://github.com/jiaweihli/monapt/tree/master/src/option), 39 | [`Future`](https://github.com/jiaweihli/monapt/tree/master/src/future), and 40 | [`Try`](https://github.com/jiaweihli/monapt/tree/master/src/try) classes are readable. 41 | 42 | You can also take a look at the [tests](https://github.com/jiaweihli/monapt/tree/master/test) to get 43 | a feel for how to use them. 44 | 45 | ## Changes in 1.0 46 | 47 | 1.0 was a complete rewrite of Monapt - including everything from the implementation to the tooling 48 | to the tests. The result is almost the same API, but more true to the original Scala interface. 49 | 50 | ## Migrating from 0.7.1 51 | 52 | ### Breaking Changes 53 | 54 | - All default exports have been removed [to avoid ambiguity](https://github.com/palantir/tslint/issues/1182#issue-151780453). 55 | - `Future` now depends on native ES6 Promises, and uses them internally when representing promises. Make sure to 56 | include a shim if you plan on using Futures. 57 | - `Future#onFailure` has been removed. 58 | - `Future#onSuccess` has been removed. 59 | - `Future#reject` has been removed. 60 | - `Monapt::flatten` has been renamed to `Option::flatten`. 61 | - `Monapt::future` has been renamed to `Future::create`. It now accepts a 62 | `when.Promise | when.Thenable | A`. 63 | - `Option#reject` has been renamed to `Option#filterNot`. 64 | - `Try#reject` has been removed. 65 | 66 | These are all backed by type definitions, so compiling your code via TypeScript should reveal any 67 | breakages. 68 | 69 | ## Credits 70 | 71 | This repo couldn't have been possible without [yaakaito/monapt](https://github.com/yaakaito/monapt). 72 | In his absence, I'll continue improving upon his hard work. 73 | -------------------------------------------------------------------------------- /src/future/future.ts: -------------------------------------------------------------------------------- 1 | import { None } from '../option/none'; 2 | import { Option } from '../option/option'; 3 | import { Try } from '../try/try'; 4 | 5 | /** 6 | * A Future represents a value which may or may not be available. It may be asynchronously filled 7 | * in the future. 8 | */ 9 | class Future { 10 | static create(value: Promise | A): Future { 11 | const isPromise: (value: Promise | A) => value is Promise = (value: Promise | A): value is Promise => { 12 | return value instanceof Promise; 13 | }; 14 | 15 | if (isPromise(value)) { 16 | return new Future(value as Promise); 17 | } 18 | else { 19 | return new Future(Promise.resolve(value)); 20 | } 21 | } 22 | 23 | /* tslint:disable:variable-name */ 24 | protected promise_: Promise; 25 | protected value_: Option>; 26 | /* tslint:enable:variable-name */ 27 | 28 | get isCompleted(): boolean { 29 | return this.value.isDefined; 30 | } 31 | 32 | get promise(): Promise { 33 | return this.promise_; 34 | } 35 | 36 | get value(): Option> { 37 | return this.value_; 38 | } 39 | 40 | protected constructor(promise: Promise) { 41 | this.value_ = None; 42 | 43 | this.promise_ = promise 44 | .then((value: A): A => { 45 | this.value_ = Option(Try(() => value)); 46 | 47 | return value; 48 | }) 49 | .catch((error: Error): Promise => { 50 | this.value_ = Option(Try(() => { throw error; })); 51 | 52 | return promise; 53 | }); 54 | } 55 | 56 | filter(predicate: (a: A) => boolean): Future { 57 | return new Future( 58 | new Promise((resolve: (a: A) => void, reject: (error: Error) => void): void => { 59 | this.promise 60 | .then((value: A): void => { 61 | if (predicate(value)) { 62 | resolve(value); 63 | } 64 | else { 65 | reject(new Error('Future.filter predicate is not satisfied')); 66 | } 67 | }) 68 | .catch((error: Error): void => { 69 | reject(error); 70 | }); 71 | }) 72 | ); 73 | } 74 | 75 | flatMap(mapper: (a: A) => Future): Future { 76 | return new Future( 77 | new Promise((resolve: (b: B) => void, reject: (error: Error) => void): void => { 78 | this.promise 79 | .then((value: A): void => { 80 | try { 81 | mapper(value).promise 82 | .then((value: B): void => { 83 | resolve(value); 84 | }) 85 | .catch((error: Error): void => { 86 | reject(error); 87 | }); 88 | } 89 | catch (error) { 90 | reject(error as Error); 91 | } 92 | }) 93 | .catch((error: Error): void => { 94 | reject(error); 95 | }); 96 | }) 97 | ); 98 | } 99 | 100 | foreach(run: (a: A) => B): void { 101 | this.promise 102 | .then((value: A): void => { 103 | run(value); 104 | }) 105 | .catch((error: Error): void => { 106 | // Don't do anything on failure. 107 | }); 108 | } 109 | 110 | map(mapper: (a: A) => B): Future { 111 | return new Future( 112 | this.promise 113 | .then((value: A): B => { 114 | return mapper(value); 115 | }) 116 | .catch((error: Error): Promise => { 117 | throw error; 118 | }) 119 | ); 120 | } 121 | 122 | onComplete(run: (t: Try) => B): void { 123 | this.promise 124 | .then( 125 | (value: A): void => { 126 | run(Try(() => value)); 127 | }) 128 | .catch((error: Error): void => { 129 | run(Try(() => { throw error; })); 130 | }); 131 | } 132 | 133 | recover(this: Future, f: (e: Error) => B): Future { 134 | return new Future( 135 | new Promise((resolve: (b: B) => void, reject: (error: Error) => void): void => { 136 | this.promise 137 | .then((value: A): void => { 138 | resolve(value); 139 | }) 140 | .catch((error: Error): void => { 141 | try { 142 | resolve(f(error)); 143 | } 144 | catch (error) { 145 | reject(error as Error); 146 | } 147 | }); 148 | }) 149 | ); 150 | } 151 | 152 | recoverWith(this: Future, f: (e: Error) => Future): Future { 153 | return new Future( 154 | new Promise((resolve: (b: B) => void, reject: (error: Error) => void): void => { 155 | this.promise 156 | .then((value: A): void => { 157 | resolve(value); 158 | }) 159 | .catch((error: Error): void => { 160 | try { 161 | f(error) 162 | .foreach((b: B): void => { 163 | resolve(b); 164 | }); 165 | } 166 | catch (error) { 167 | reject(error as Error); 168 | } 169 | }); 170 | }) 171 | ); 172 | } 173 | 174 | zip(that: Future): Future<[A, B]> { 175 | return this.flatMap((value: A) => that.map((thatValue: B) => [value, thatValue] as [A, B])); 176 | } 177 | 178 | } 179 | 180 | export { Future }; 181 | -------------------------------------------------------------------------------- /test/future/future.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, test } from 'ava'; 2 | 3 | import { Future } from '../../src/future/future'; 4 | import { Option } from '../../src/option/option'; 5 | import { Try } from '../../src/try/try'; 6 | 7 | const error: Error = new Error('sample error'); 8 | 9 | const success: Future = Future.create('hello'); 10 | const failure: Future = Future.create(Promise.reject(error)); 11 | 12 | test('Future::create', async(t: ExecutionContext) => { 13 | t.plan(2); 14 | 15 | const value: string = 'hello'; 16 | 17 | const futureFromPromise: Future = Future.create(Promise.resolve(value)); 18 | const futureFromValue: Future = Future.create(value); 19 | 20 | await futureFromPromise.promise; 21 | t.deepEqual(futureFromPromise.value, Option(Try(() => value))); 22 | 23 | await futureFromValue.promise; 24 | t.deepEqual(futureFromValue.value, Option(Try(() => value))); 25 | }); 26 | 27 | test('Future#filter', async(t: ExecutionContext) => { 28 | t.plan(3); 29 | 30 | const successfulFilter: string = await success.filter((v: string) => v === 'hello').promise; 31 | t.is(successfulFilter, 'hello'); 32 | 33 | await t.throws(failure.filter((v: string) => v === 'hello').promise); 34 | await t.throws(success.filter((v: string) => v === 'world').promise); 35 | }); 36 | 37 | test('Future#flatMap', async(t: ExecutionContext) => { 38 | t.plan(4); 39 | 40 | const successfulFlatMap: string = await success.flatMap(() => Future.create('world')).promise; 41 | t.is(successfulFlatMap, 'world'); 42 | 43 | const failingFlatMap: PromiseLike = success.flatMap((): Future => { throw error; }).promise; 44 | await t.throws(failingFlatMap); 45 | 46 | await t.throws(failure.flatMap(() => success).promise); 47 | await t.throws(success.flatMap(() => failure).promise); 48 | }); 49 | 50 | test('Future#foreach', async(t: ExecutionContext) => { 51 | t.plan(2); 52 | 53 | let executions: number = 0; 54 | 55 | success.foreach(() => { 56 | executions += 1; 57 | }); 58 | 59 | await success.promise; 60 | 61 | t.is(executions, 1); 62 | 63 | executions = 0; 64 | 65 | failure.foreach(() => { 66 | executions += 1; 67 | }); 68 | 69 | try { 70 | await failure.promise; 71 | } 72 | catch (e) { 73 | // No-op: ignore exception 74 | } 75 | 76 | t.is(executions, 0); 77 | }); 78 | 79 | test('Future#isCompleted', async(t: ExecutionContext) => { 80 | t.plan(2); 81 | 82 | const delayedFuture: Future = Future.create( 83 | new Promise((resolve: (value: void) => void): void => { 84 | setTimeout(resolve, 0); 85 | }) 86 | ); 87 | 88 | t.is(delayedFuture.isCompleted, false); 89 | 90 | await delayedFuture.promise; 91 | 92 | t.is(delayedFuture.isCompleted, true); 93 | }); 94 | 95 | test('Future#map', async(t: ExecutionContext) => { 96 | t.plan(2); 97 | 98 | const successfulMap: string = await success.map((v: string) => `${v} world`).promise; 99 | t.is(successfulMap, 'hello world'); 100 | 101 | await t.throws(failure.map(() => 'world').promise); 102 | }); 103 | 104 | test('Future#onComplete', async(t: ExecutionContext) => { 105 | t.plan(2); 106 | 107 | success.onComplete((v: Try) => { 108 | t.true(v.isSuccess); 109 | }); 110 | 111 | await success.promise; 112 | 113 | failure.onComplete((v: Try) => { 114 | t.false(v.isSuccess); 115 | }); 116 | 117 | try { 118 | await failure.promise; 119 | } 120 | catch (error) { 121 | // Do nothing, the failure throwing an error is expected. 122 | } 123 | }); 124 | 125 | test('Future#recover', async(t: ExecutionContext) => { 126 | t.plan(4); 127 | 128 | const passthroughRecover: string = await success.recover((e: Error) => { 129 | if (e === error) { 130 | return 'world'; 131 | } 132 | else { 133 | throw error; 134 | } 135 | }).promise; 136 | t.is(passthroughRecover, 'hello'); 137 | 138 | const successfulRecover: string = await failure.recover((e: Error) => { 139 | if (e === error) { 140 | return 'world'; 141 | } 142 | else { 143 | throw error; 144 | } 145 | }).promise; 146 | t.is(successfulRecover, 'world'); 147 | 148 | await t.throws( 149 | failure.recover((e: Error) => { 150 | if (e !== error) { 151 | return 'world'; 152 | } 153 | else { 154 | throw error; 155 | } 156 | }).promise 157 | ); 158 | 159 | await t.throws(failure.recover((e: Error): string => { throw error; }).promise); 160 | }); 161 | 162 | test('Future#recoverWith', async(t: ExecutionContext) => { 163 | t.plan(4); 164 | 165 | const passthroughRecoverWith: string = await success.recoverWith((e: Error) => { 166 | if (e === error) { 167 | return Future.create('world'); 168 | } 169 | else { 170 | throw error; 171 | } 172 | }).promise; 173 | t.is(passthroughRecoverWith, 'hello'); 174 | 175 | const successfulRecoverWith: string = await failure.recoverWith((e: Error) => { 176 | if (e === error) { 177 | return Future.create('world'); 178 | } 179 | else { 180 | throw error; 181 | } 182 | }).promise; 183 | t.is(successfulRecoverWith, 'world'); 184 | 185 | await t.throws( 186 | failure.recoverWith((e: Error) => { 187 | if (e !== error) { 188 | return Future.create('world'); 189 | } 190 | else { 191 | throw error; 192 | } 193 | }).promise as PromiseLike 194 | ); 195 | 196 | await t.throws(failure.recoverWith((e: Error): Future => { throw error; }).promise); 197 | }); 198 | 199 | test('Future#zip', async(t: ExecutionContext) => { 200 | t.plan(3); 201 | 202 | const value: Future = Future.create(10); 203 | const successfulZip: [string, number] = await success.zip(value).promise; 204 | t.deepEqual(successfulZip, ['hello', 10]); 205 | 206 | await t.throws(failure.zip(value).promise); 207 | await t.throws(value.zip(failure).promise); 208 | }); 209 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "adjacent-overload-signatures": true, 4 | "align": false, 5 | "array-type": [ 6 | true, 7 | "generic" 8 | ], 9 | "arrow-parens": true, 10 | "arrow-return-shorthand": [true], 11 | "await-promise": true, 12 | "callable-types": true, 13 | "class-name": true, 14 | "comment-format": [ 15 | true, 16 | "check-space", 17 | "check-uppercase" 18 | ], 19 | "completed-docs": [true, { 20 | "classes": true, 21 | "enums": true, 22 | "interfaces": true 23 | }], 24 | "curly": true, 25 | "cyclomatic-complexity": false, 26 | "eofline": true, 27 | "forin": true, 28 | "import-spacing": true, 29 | "indent": [ 30 | true, 31 | "spaces" 32 | ], 33 | "interface-name": [true, "never-prefix"], 34 | "interface-over-type-literal": true, 35 | "jsdoc-format": true, 36 | "label-position": true, 37 | "linebreak-style": [ 38 | true, 39 | "LF" 40 | ], 41 | "max-classes-per-file": [false], 42 | "max-file-line-count": [false], 43 | "max-line-length": [true, 120], 44 | "match-default-export-name": true, 45 | "member-ordering": [ 46 | true, 47 | { 48 | "order": [ 49 | "public-static-field", 50 | "public-static-method", 51 | "protected-static-field", 52 | "protected-static-method", 53 | "private-static-field", 54 | "private-static-method", 55 | "public-instance-field", 56 | "protected-instance-field", 57 | "private-instance-field", 58 | "public-constructor", 59 | "protected-constructor", 60 | "private-constructor", 61 | "public-instance-method", 62 | "protected-instance-method", 63 | "private-instance-method" 64 | ] 65 | } 66 | ], 67 | "newline-before-return": true, 68 | "new-parens": true, 69 | "no-angle-bracket-type-assertion": true, 70 | "no-any": true, 71 | "no-arg": true, 72 | "no-bitwise": true, 73 | "no-boolean-literal-compare": true, 74 | "no-conditional-assignment": true, 75 | "no-consecutive-blank-lines": true, 76 | "no-console": true, 77 | "no-construct": true, 78 | "no-debugger": true, 79 | "no-default-export": true, 80 | "no-duplicate-super": true, 81 | "no-duplicate-variable": true, 82 | "no-empty": true, 83 | "no-empty-interface": true, 84 | "no-eval": true, 85 | // Monapt wraps promises and lets you chain them, so there'll be floating promises. 86 | "no-floating-promises": false, 87 | "no-for-in-array": true, 88 | "no-import-side-effect": true, 89 | "no-inferred-empty-object-type": true, 90 | "no-internal-module": true, 91 | "no-invalid-template-strings": true, 92 | "no-invalid-this": [ 93 | true, 94 | "check-function-in-method" 95 | ], 96 | "no-magic-numbers": true, 97 | "no-mergeable-namespace": true, 98 | "no-misused-new": true, 99 | "no-namespace": true, 100 | "no-non-null-assertion": true, 101 | "no-null-keyword": true, 102 | "no-parameter-properties": true, 103 | "no-require-imports": true, 104 | "no-reference": true, 105 | "no-reference-import": true, 106 | "no-sparse-arrays": true, 107 | "no-string-literal": true, 108 | "no-string-throw": true, 109 | "no-switch-case-fall-through": true, 110 | "no-trailing-whitespace": true, 111 | "no-unbound-method": true, 112 | "no-unnecessary-callback-wrapper": false, 113 | "no-unnecessary-initializer": true, 114 | // Seems to be buggy. Sample error: `Warning: Cannot read property 'some' of undefined` 115 | "no-unnecessary-qualifier": false, 116 | "no-unsafe-any": true, 117 | "no-unsafe-finally": true, 118 | "no-unused-expression": true, 119 | "no-unused-variable": true, 120 | "no-use-before-declare": true, 121 | "no-var-keyword": true, 122 | "no-var-requires": true, 123 | "no-void-expression": true, 124 | "object-literal-key-quotes": [ 125 | true, 126 | "as-needed" 127 | ], 128 | "object-literal-shorthand": true, 129 | "object-literal-sort-keys": false, 130 | "one-line": [ 131 | true, 132 | "check-open-brace", 133 | "check-whitespace" 134 | ], 135 | "one-variable-per-declaration": true, 136 | "only-arrow-functions": true, 137 | "ordered-imports": [ 138 | true, 139 | { 140 | "import-sources-order": "any", 141 | "named-imports-order": "case-insensitive" 142 | } 143 | ], 144 | "prefer-const": true, 145 | "prefer-for-of": true, 146 | "promise-function-async": false, 147 | // Sometimes when you implement interfaces you don't need `this`. 148 | "prefer-function-over-method": false, 149 | // Can sometimes make things hard to read. 150 | "prefer-method-signature": false, 151 | "prefer-template": true, 152 | "quotemark": [ 153 | true, 154 | "single", 155 | "avoid-escape" 156 | ], 157 | "radix": true, 158 | "restrict-plus-operands": true, 159 | "return-undefined": true, 160 | "semicolon": [ 161 | true, 162 | "always" 163 | ], 164 | "space-before-function-paren": [true, "never"], 165 | "strict-boolean-expressions": true, 166 | "strict-type-predicates": true, 167 | "switch-default": true, 168 | "trailing-comma": [ 169 | true, 170 | { 171 | "multiline": "never", 172 | "singleline": "never" 173 | } 174 | ], 175 | "triple-equals": true, 176 | "typedef": [ 177 | true, 178 | "call-signature", 179 | "arrow-call-signature", 180 | "parameter", 181 | "arrow-parameter", 182 | "property-declaration", 183 | "variable-declaration", 184 | "member-variable-declaration", 185 | "object-destructuring", 186 | "array-destructuring" 187 | ], 188 | "typedef-whitespace": [ 189 | true, 190 | { 191 | "call-signature": "nospace", 192 | "index-signature": "nospace", 193 | "parameter": "nospace", 194 | "property-declaration": "nospace", 195 | "variable-declaration": "nospace" 196 | }, 197 | { 198 | "call-signature": "onespace", 199 | "index-signature": "onespace", 200 | "parameter": "onespace", 201 | "property-declaration": "onespace", 202 | "variable-declaration": "onespace" 203 | } 204 | ], 205 | "typeof-compare": true, 206 | // Can make method bodies hard to read when a lot of the logic is spent on parsing. 207 | "unified-signatures": false, 208 | "use-isnan": true, 209 | "variable-name": [ 210 | true, 211 | "check-format", 212 | "allow-trailing-underscore", 213 | "ban-keywords" 214 | ], 215 | "whitespace": [ 216 | true, 217 | "check-branch", 218 | "check-decl", 219 | "check-operator", 220 | "check-module", 221 | "check-separator", 222 | "check-type", 223 | "check-preblock" 224 | ] 225 | } 226 | } 227 | --------------------------------------------------------------------------------