├── .github └── workflows │ └── npm-publish.yaml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── optional.spec.ts ├── optional.ts └── types.ts ├── tsconfig.json └── tslint.json /.github/workflows/npm-publish.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: 14 19 | cache: npm 20 | - run: npm ci 21 | - run: npm run lint 22 | - run: npm run test:coverage 23 | - run: npm run codecov 24 | env: 25 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 26 | 27 | publish-npm: 28 | if: github.ref_type == 'tag' 29 | needs: test 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions/setup-node@v2 34 | with: 35 | node-version: 14 36 | registry-url: https://registry.npmjs.org/ 37 | cache: npm 38 | - run: npm run build 39 | - run: npm publish 40 | env: 41 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .nyc_output 3 | node_modules 4 | coverage 5 | dist 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | 3 | .git 4 | .github 5 | coverage 6 | src 7 | 8 | .gitignore 9 | jest.config.js 10 | tsconfig.json 11 | tslint.json 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2022 bromne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript Optional 2 | 3 | [![npm](https://img.shields.io/npm/v/typescript-optional.svg)](https://www.npmjs.com/package/typescript-optional) 4 | [![License](https://img.shields.io/npm/l/typescript-optional.svg)](https://www.npmjs.com/package/typescript-optional) 5 | [![codecov](https://codecov.io/gh/bromne/typescript-optional/branch/master/graph/badge.svg?token=vwg9UEGoic)](https://codecov.io/gh/bromne/typescript-optional) 6 | 7 | Optional (like Java) implementation in TypeScript 8 | 9 | ## Overview 10 | 11 | `Optional` is a type which *may* or *may not* contain a *payload* of type `T`. 12 | It provides a common interface regardless of whether an instance is *present* or is *empty*. 13 | 14 | This module is inspired by [Optional class in Java 8+](https://docs.oracle.com/javase/10/docs/api/java/util/Optional.html). 15 | 16 | The following methods are currently not supported: 17 | 18 | - `equals` 19 | - `toString` 20 | - `hashCode` 21 | - `stream` 22 | 23 | ### Install 24 | 25 | ``` 26 | npm install --save typescript-optional 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### import 32 | 33 | ```ts 34 | // import Optional type from this module 35 | import { Optional } from "typescript-optional"; 36 | ``` 37 | 38 | ### creating `Optional` objects 39 | 40 | ```ts 41 | const nullableString: string | null = /* some nullable value */; 42 | 43 | // all the following variables will be parameterized as Optional. 44 | const optional = Optional.ofNullable(nullableString); 45 | const optionalPresent1 = Optional.ofNullable("foo"); 46 | const optionalPresent2 = Optional.ofNonNull("foo"); // accepts non-null value (or else throws TypeError) 47 | const optionalEmpty1: Optional = Optional.empty(); // type hinting required 48 | const optionalEmpty2 = Optional.empty(); // or parameterize explicitly 49 | ``` 50 | 51 | ### operations 52 | 53 | ```ts 54 | const optional: Optional = Optional.ofNullable( /* some optional value: null | string */ ); 55 | 56 | // force to retrieve the payload. (or else throws TypeError.) 57 | // this method is not used match. 58 | optional.get(); 59 | 60 | // represent whether this is present or not. 61 | optional.isPresent(); 62 | 63 | // represent whether this is empty or not. (negation of `isPresent` property) 64 | optional.isEmpty(); 65 | 66 | // if a payload is present, execute the given `consumer`. 67 | optional.ifPresent(value => console.log(value)); 68 | 69 | // if a payload is present, execute the first argument (`consumer`), 70 | // otherwise execute the second argument (emptyAction). 71 | optional.ifPresentOrElse(value => console.log(value), () => console.log("empty")); 72 | 73 | // filter a payload with additional predicate. 74 | optional.filter(value => value.length > 0); 75 | 76 | // map a payload with the given mapper. 77 | optional.map(value => value.length); 78 | 79 | // map a payload with the given mapper which returns value wrapped with Optional type. 80 | const powerIfPositive: (x: Number) => Optional 81 | = x => (x > 0) ? Optional.ofNonNull(x * x) : Optional.empty(); 82 | const numberOptional: Optional = Optional.ofNullable(/* some optional value: null | number */) 83 | numberOptional.flatMap(value => powerIfPositive(value as number)); 84 | 85 | // if this is present, return this, otherwise return the given another optional. 86 | const another: Optional = Optional.ofNullable(/* ... */); 87 | optional.or(another); 88 | 89 | // if this is present retrieve the payload, 90 | // otherwise return the given value. 91 | optional.orElse("bar"); 92 | 93 | // if a payload is present, retrieve the payload, 94 | // otherwise return a value supplied by the given function. 95 | optional.orElseGet(() => "bar"); 96 | 97 | // if a payload is present, retrieve the payload, 98 | // otherwise throw an exception supplied by the given function. 99 | optional.orElseThrow(() => new Error()); 100 | 101 | // if a payload is present, retrieve the payload, 102 | // otherwise return null. 103 | optional.orNull(); 104 | 105 | // if a payload is present, retrieve the payload, 106 | // otherwise return undefined. 107 | optional.orUndefined(); 108 | 109 | // return an appropriate result by emulating pattern matching with the given cases. 110 | optional.matches({ 111 | present: value => value.length, 112 | empty: () => 0, 113 | }) 114 | 115 | // convert this to an Option value. 116 | optional.toOption(); 117 | ``` 118 | 119 | ### prototype-free types 120 | 121 | While `Optional`'s fluent interface for method chaining with `prototype` is usually useful and elegant, 122 | relying on `prototype` can cause some problems in certain situations like that an external function copies such objects *except* `prototype`. 123 | For example, `setState` of React reflects the given value as a state except the value's `prototype` (and then you will see "TypeError: undefined is not a function" in runtime though TypeScript compilation has been succeeded!). 124 | 125 | To avoid this issue, you have three options that *convert* an `Optional` into a *prototype-free*, or a simple JavaScript object (associative array, string etc.). 126 | 127 | - `Optional.orNull` 128 | - `Optional.orUndefined` 129 | - `Optional.toOption` 130 | 131 | #### `Optional.orNull` and `Optional.orUndefined` 132 | 133 | Using `Optional.orNull` or `Optional.orUndefined` is the simple way to obtain prototype-free objects. 134 | These methods convert an `Optional` into a value of *type union*. 135 | 136 | `Optional.orNull` returns `T | null`. 137 | 138 | `Optional.orUndefined` returns `T | undefined`. The `T | undefined` type is compatible with [optional parameters and properties](http://www.typescriptlang.org/docs/handbook/advanced-types.html#optional-parameters-and-properties) of TypeScript. 139 | 140 | Use `Optional.ofNullable` to restore an Optional value from a value of these type unions. 141 | 142 | ```ts 143 | const update: (original: T) => T = /* some external function that returns without the prototype */ 144 | const optional: Optional = /* some Optional object */; 145 | 146 | let nullable: string | null = optional.orNull(); 147 | let orUndefined: string | undefined = optional.orUndefined(); 148 | 149 | // update using external functions! 150 | nullable = update(nullable); 151 | orUndefined = update(orUndefined); 152 | 153 | // retore from (string | null). 154 | const optionalFromNullable: Optional = Optional.ofNullble(nullable); 155 | 156 | // restore from (string | undefined). 157 | const optionalFromOrUndefined: Optional = Optional.ofNullble(orUndefined); 158 | ``` 159 | 160 | #### `Option.toOption` 161 | 162 | As a more explicit way to obtain prototype-free objects, `Optional.toOption` is provided. 163 | This method convert an `Optional` into an object of `Option` type, which conforms to [*discriminated unions*](http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions) also known as *algebraic data types*. 164 | Refer the [API document](lib/types.ts) of `Option` to learn about the structure. 165 | 166 | ```ts 167 | const update: (original: Option) => T = /* some external function that returns without the prototype */ 168 | const optional: Optional = /* some Optional value */; 169 | 170 | let option: Option = optional.toOption(); 171 | 172 | // update using external functions! 173 | option = update(option); 174 | 175 | // restore from Option. 176 | const optionalFromOption: Optional = Optional.from(option); 177 | ``` 178 | 179 | ## License 180 | 181 | MIT License - [LICENSE.md](LICENSE.md) 182 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-optional", 3 | "version": "3.0.0-alpha.1", 4 | "description": "Optional (like Java) implementation in TypeScript", 5 | "keywords": [ 6 | "java", 7 | "optional", 8 | "typescript" 9 | ], 10 | "author": "bromne ", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/bromne/typescript-optional.git" 15 | }, 16 | "main": "dist/cjs/index.js", 17 | "module": "dist/esm/index.js", 18 | "types": "dist/esm/index.d.ts", 19 | "sideEffects": false, 20 | "scripts": { 21 | "lint": "tslint lib/**/*.ts test/**/*.ts", 22 | "build": "npm run build:esm && npm run build:cjs", 23 | "build:esm": "tsc", 24 | "build:cjs": "tsc --module CommonJS --outDir dist/cjs/", 25 | "test": "jest", 26 | "test:coverage": "jest --coverage", 27 | "codecov": "codecov" 28 | }, 29 | "devDependencies": { 30 | "@types/jest": "^27.4.0", 31 | "codecov": "^3.8.3", 32 | "jest": "^27.4.5", 33 | "ts-jest": "^27.1.2", 34 | "tslint": "^5.12.0", 35 | "typescript": "^3.9.10" 36 | }, 37 | "jest": { 38 | "preset": "ts-jest" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Optional } from "./optional.js"; 2 | export { Cases, Empty, Option, Present } from "./types.js"; 3 | -------------------------------------------------------------------------------- /src/optional.spec.ts: -------------------------------------------------------------------------------- 1 | import { Optional } from "./optional"; 2 | import { Cases, Option } from "./types"; 3 | 4 | describe("Optional", () => { 5 | const payload: string = "foo"; 6 | const sutPresent: Optional = Optional.ofNonNull(payload); 7 | const sutEmpty: Optional = Optional.empty(); 8 | 9 | describe("#of", () => { 10 | it("should return a present Optional when it is given a non-null value.", () => { 11 | const sut = Optional.of("foo"); 12 | expect(sut.isPresent()).toBe(true); 13 | }); 14 | 15 | it("should throw an exception when it is given null.", () => { 16 | expect(() => Optional.of(null)).toThrow(); 17 | }); 18 | 19 | it("should throw an exception when it is given undefined.", () => { 20 | expect(() => Optional.of(undefined)).toThrow(); 21 | }); 22 | }); 23 | 24 | describe("#ofNonNull", () => { 25 | it("should return a present Optional when it is given a non-null value.", () => { 26 | const sut = Optional.ofNonNull("foo"); 27 | expect(sut.isPresent()).toBe(true); 28 | }); 29 | 30 | it("should throw an exception when it is given null.", () => { 31 | expect(() => Optional.ofNonNull(null)).toThrow(); 32 | }); 33 | 34 | it("should throw an exception when it is given undefined.", () => { 35 | expect(() => Optional.ofNonNull(undefined)).toThrow(); 36 | }); 37 | }); 38 | 39 | describe("#ofNullable", () => { 40 | const getNullable: () => string | null = () => null; 41 | const getUndefinedable: () => string | undefined = () => undefined; 42 | 43 | it("should return a present Optional when it is given a non-null value.", () => { 44 | const sut = Optional.ofNullable("foo"); 45 | expect(sut.isPresent()).toBe(true); 46 | }); 47 | 48 | it("should return an empty Optional when it receives null.", () => { 49 | const sut: Optional = Optional.ofNullable(getNullable()); 50 | expect(sut.isEmpty()).toBe(true); 51 | }); 52 | 53 | it("should return an empty Optional when it receives undefined.", () => { 54 | const sut: Optional = Optional.ofNullable(getUndefinedable()); 55 | expect(sut.isEmpty()).toBe(true); 56 | }); 57 | }); 58 | 59 | describe("#from", () => { 60 | it("returns a present optional when it is given a present option.", () => { 61 | const presentOption: Option = { kind: "present", value: payload }; 62 | const sut = Optional.from(presentOption); 63 | expect(sut.isPresent()).toBe(true); 64 | }); 65 | 66 | it("returns an empty optional when it is given an empty option.", () => { 67 | const emptyOption: Option = { kind: "empty" }; 68 | const sut = Optional.from(emptyOption); 69 | expect(sut.isEmpty()).toBe(true); 70 | }); 71 | 72 | it("throws an exception when it is given a value which is not Option type.", () => { 73 | const malformed: any = {}; 74 | expect(() => Optional.from(malformed)).toThrow(); 75 | }); 76 | }); 77 | 78 | describe("#empty", () => { 79 | it("should return an empty Optional.", () => { 80 | const sut: Optional = Optional.empty(); 81 | expect(sut.isEmpty()).toBe(true); 82 | }); 83 | }); 84 | 85 | describe("#isPresent", () => { 86 | it("should return true if it is present", () => { 87 | expect(sutPresent.isPresent()).toBe(true); 88 | }); 89 | 90 | it("should return false if it is not present", () => { 91 | expect(sutEmpty.isPresent()).toBe(false); 92 | }); 93 | }); 94 | 95 | describe("#isEmpty", () => { 96 | it("should return false if it is present", () => { 97 | expect(sutPresent.isEmpty()).toBe(false); 98 | }); 99 | 100 | it("should return true if it is not present", () => { 101 | expect(sutEmpty.isEmpty()).toBe(true); 102 | }); 103 | }); 104 | 105 | describe("#get", () => { 106 | it("should return the payload if it is present.", () => { 107 | expect(sutPresent.get()).toBe(payload); 108 | }); 109 | 110 | it("should throw an exception if it is empty.", () => { 111 | expect(() => sutEmpty.get()).toThrow(); 112 | }); 113 | }); 114 | 115 | describe("#ifPresent", () => { 116 | it("should call the given function if it is present.", () => { 117 | let called = false; 118 | sutPresent.ifPresent(value => { 119 | expect(value).toBe(payload); 120 | called = true; 121 | }); 122 | expect(called).toBe(true); 123 | }); 124 | 125 | it("should not call the given function if it is empty.", () => { 126 | let called = false; 127 | sutEmpty.ifPresent(value => { 128 | called = true; 129 | }); 130 | expect(called).toBe(false); 131 | }); 132 | }); 133 | 134 | describe("#ifPresentOrElse", () => { 135 | it("should call the given function if it is present.", () => { 136 | let called = false; 137 | let calledEmpty = false; 138 | sutPresent.ifPresentOrElse(value => { 139 | expect(value).toBe(payload); 140 | called = true; 141 | }, () => { 142 | calledEmpty = true; 143 | }); 144 | expect(called).toBe(true); 145 | expect(calledEmpty).toBe(false); 146 | }); 147 | 148 | it("should call the emptyAction function if it is empty.", () => { 149 | let called = false; 150 | let calledEmpty = false; 151 | sutEmpty.ifPresentOrElse(value => { 152 | called = true; 153 | }, () => { 154 | calledEmpty = true; 155 | }); 156 | expect(called).toBe(false); 157 | expect(calledEmpty).toBe(true); 158 | }); 159 | }); 160 | 161 | describe("#filter", () => { 162 | it("should return a present Optional if it is present and the predicate should return true.", () => { 163 | const actual = sutPresent.filter(value => value.length > 0); 164 | expect(actual.get()).toBe(payload); 165 | }); 166 | 167 | it("should return an empty Optional if it is present and the predicate should return false.", () => { 168 | const actual = sutPresent.filter(value => value.length === 0); 169 | expect(actual.isEmpty()).toBe(true); 170 | }); 171 | 172 | it("should return an empty Optional if it is empty.", () => { 173 | const actual = sutEmpty.filter(value => true); 174 | expect(actual.isEmpty()).toBe(true); 175 | }); 176 | }); 177 | 178 | describe("#map", () => { 179 | const mapper = (value: string) => value.length; 180 | 181 | it("should return a present Optional whose payload is mapped by the given function " 182 | + "if it is present and the result is not null.", () => { 183 | const actual = sutPresent.map(mapper).get(); 184 | const expected = mapper(payload); 185 | expect(actual).toBe(expected); 186 | }); 187 | 188 | it("should return an empty Optional if it is empty.", () => { 189 | const actual = sutEmpty.map(mapper); 190 | expect(actual.isEmpty()).toBe(true); 191 | }); 192 | 193 | it("should handle null/undefined mapper return value properly", () => { 194 | interface Payload { 195 | a: string; 196 | } 197 | 198 | const p: Payload = { 199 | a: "A", 200 | }; 201 | expect(p.a).toBe(Optional.ofNonNull(p).map(x => x.a).get()); 202 | 203 | const fallback = "B"; 204 | expect(fallback).toBe(Optional.ofNonNull(p as any).map(x => x.b).orElse(fallback)); 205 | expect(fallback).toBe(Optional.ofNonNull(p).map(x => null as any).orElse(fallback)); 206 | 207 | const m: (x: boolean) => undefined | Payload = (isNull: boolean) => isNull ? undefined : p; 208 | 209 | const fallbackPayload = { 210 | a: "B", 211 | }; 212 | 213 | expect(fallbackPayload) 214 | .toBe(Optional.ofNonNull(p).map(x => m(true)).orElse(fallbackPayload)); 215 | expect(p.a).toBe(Optional.ofNonNull(p).map(x => m(false)).map(x => x.a).get()); 216 | }); 217 | }); 218 | 219 | describe("#flatMap", () => { 220 | { 221 | const sqrtIfNonNegative: (x: number) => Optional = x => { 222 | if (x >= 0) 223 | return Optional.ofNonNull(Math.sqrt(x)); 224 | else 225 | return Optional.empty(); 226 | }; 227 | 228 | it("should return the present Optional which is mapped by the given function " 229 | + "if it is present and the function should return present.", () => { 230 | const nonNegative = 16; 231 | const sut = Optional.ofNonNull(nonNegative); 232 | const actual = sut.flatMap(sqrtIfNonNegative); 233 | expect(actual.get()).toBe(Math.sqrt(nonNegative)); 234 | }); 235 | 236 | it("should return the empty Optional which is mapped by the given function " 237 | + "if it is present and the function should return empty.", () => { 238 | const negative = -16; 239 | const sut = Optional.ofNonNull(negative); 240 | const actual = sut.flatMap(sqrtIfNonNegative); 241 | expect(actual.isEmpty()).toBe(true); 242 | }); 243 | 244 | it("should return the empty Optional if it is empty.", () => { 245 | const sut = Optional.empty(); 246 | const actual = sut.flatMap(sqrtIfNonNegative); 247 | expect(actual.isEmpty()).toBe(true); 248 | }); 249 | } 250 | { 251 | const left: Optional = Optional.ofNonNull(3); 252 | const right: Optional = Optional.ofNonNull(4); 253 | const empty: Optional = Optional.empty(); 254 | const sum = left.get() + right.get(); 255 | 256 | // tslint:disable-next-line:max-line-length 257 | it("should return value of applying bi-function to two payloads if both of the two Optionals are present.", () => { 258 | const actual = left.flatMap(x => right.map(y => x + y)); 259 | expect(actual.get()).toBe(sum); 260 | }); 261 | 262 | it("should return empty Optional if the left is present and the right is empty.", () => { 263 | const actual = left.flatMap(x => empty.map(y => x + y)); 264 | expect(actual.isEmpty()).toBe(true); 265 | }); 266 | 267 | it("should return empty Optional if the left is empty and the right is present.", () => { 268 | const actual = empty.flatMap(x => right.map(y => x + y)); 269 | expect(actual.isEmpty()).toBe(true); 270 | }); 271 | } 272 | }); 273 | 274 | describe("#or", () => { 275 | const another = "bar"; 276 | const supplier = () => Optional.ofNonNull(another); 277 | 278 | it("should return the current Optional if it is present.", () => { 279 | const actual = sutPresent.or(supplier); 280 | expect(actual === sutPresent).toBe(true); 281 | }); 282 | 283 | it("should return the supplier's value if it is empty.", () => { 284 | const actual = sutEmpty.or(supplier); 285 | expect(actual.get()).toBe(another); 286 | }); 287 | }); 288 | 289 | describe("#orElse", () => { 290 | const another = "bar"; 291 | 292 | it("should return the original payload if it is present.", () => { 293 | const actual = sutPresent.orElse(another); 294 | expect(actual).toBe(sutPresent.get()); 295 | }); 296 | 297 | it("should return the given value if it is empty.", () => { 298 | const actual = sutEmpty.orElse(another); 299 | expect(actual).toBe(another); 300 | }); 301 | }); 302 | 303 | describe("#orElseGet", () => { 304 | const another = "bar"; 305 | 306 | it("should return the original payload if it is present.", () => { 307 | const actual = sutPresent.orElseGet(() => another); 308 | expect(actual).toBe(sutPresent.get()); 309 | }); 310 | 311 | it("should return the value returned by the given function if it is empty.", () => { 312 | const actual = sutEmpty.orElseGet(() => another); 313 | expect(actual).toBe(another); 314 | }); 315 | }); 316 | 317 | describe("#orElseThrow", () => { 318 | it("should return the original payload if it is present.", () => { 319 | const actual = sutPresent.orElseThrow(TypeError); 320 | expect(actual).toBe(sutPresent.get()); 321 | }); 322 | 323 | it("should throw the exception returned by the given function if it is empty.", () => { 324 | expect(() => sutEmpty.orElseThrow(TypeError)).toThrow(); 325 | }); 326 | }); 327 | 328 | describe("#orNull", () => { 329 | it("returns the original payload when it is present.", () => { 330 | const actual = sutPresent.orNull(); 331 | expect(actual).toBe(sutPresent.get()); 332 | }); 333 | 334 | it("returns null when it is empty.", () => { 335 | const actual = sutEmpty.orNull(); 336 | expect(actual).toBe(null); 337 | }); 338 | }); 339 | 340 | describe("#orUndefined", () => { 341 | it("returns the original payload when it is present.", () => { 342 | const actual = sutPresent.orUndefined(); 343 | expect(actual).toBe(sutPresent.get()); 344 | }); 345 | 346 | it("returns null when it is empty.", () => { 347 | const actual = sutEmpty.orUndefined(); 348 | expect(actual).toBe(undefined); 349 | }); 350 | }); 351 | 352 | describe("#toOption", () => { 353 | it("returns a Some value when it is present.", () => { 354 | const actual = sutPresent.toOption(); 355 | expect(actual.kind).toBe("present"); 356 | }); 357 | 358 | it("returns a None value when it is empty.", () => { 359 | const actual = sutEmpty.toOption(); 360 | expect(actual.kind).toBe("empty"); 361 | }); 362 | }); 363 | 364 | describe("#matches", () => { 365 | const cases: Cases = { 366 | present: x => x.length, 367 | empty: () => 0, 368 | }; 369 | 370 | it("returns a value converted from the payload by the given 'present' function when it is present", () => { 371 | const actual = sutPresent.matches(cases); 372 | const expected = cases.present(payload); 373 | expect(actual).toBe(expected); 374 | }); 375 | 376 | it("returns the value returned by the given 'empty' function when it is empty.", () => { 377 | const actual = sutEmpty.matches(cases); 378 | const expected = cases.empty(); 379 | expect(actual).toBe(expected); 380 | }); 381 | }); 382 | 383 | describe("#toJSON", () => { 384 | it("returns a value of payload itself when it is present.", () => { 385 | const sut = { foo: sutPresent }; 386 | const actual = JSON.parse(JSON.stringify(sut)); 387 | const expected = { foo: sutPresent.get() }; 388 | expect(actual).toStrictEqual(expected); 389 | }); 390 | 391 | it("returns null when it is empty.", () => { 392 | const sut = { foo: sutEmpty }; 393 | const actual = JSON.parse(JSON.stringify(sut)); 394 | const expected = { foo: null }; 395 | expect(actual).toStrictEqual(expected); 396 | }); 397 | }); 398 | }); 399 | -------------------------------------------------------------------------------- /src/optional.ts: -------------------------------------------------------------------------------- 1 | import { Cases, Option } from "./types.js"; 2 | 3 | /** 4 | * `Optional` (like Java) implementation in TypeScript. 5 | * 6 | * `Optional` is a type which *may* or *may not* contain a *payload* of type `T`. 7 | * It provides a common interface regardless of whether an instance is *present* or is *empty*. 8 | * 9 | * This module is inspired by 10 | * [Optional class in Java 8+](https://docs.oracle.com/javase/10/docs/api/java/util/Optional.html). 11 | * 12 | * The following methods are currently not supported: 13 | * 14 | * - `equals` 15 | * - `toString` 16 | * - `hashCode` 17 | * - `stream` 18 | */ 19 | export abstract class Optional { 20 | /** 21 | * Returns whether this is present or not. 22 | * 23 | * If a payload is present, be `true` , otherwise be `false`. 24 | */ 25 | abstract isPresent(): boolean; 26 | 27 | /** 28 | * Returns whether this is empty or not. 29 | * 30 | * If this is empty, be `true`, otherwise be `false`. 31 | * This method is negation of `Optional#isPresent`. 32 | */ 33 | isEmpty(): boolean { 34 | return !this.isPresent(); 35 | } 36 | 37 | /** 38 | * Force to retrieve the payload. 39 | * If a payload is present, returns the payload, otherwise throws `TypeError`. 40 | * 41 | * @throws {TypeError} if this is empty. 42 | */ 43 | abstract get(): T; 44 | 45 | /** 46 | * If a payload is present, executes the given `consumer`, otherwise does nothing. 47 | * 48 | * @param consumer a consumer of the payload 49 | */ 50 | abstract ifPresent(consumer: (value: T) => void): void; 51 | 52 | /** 53 | * If a payload is present, executes the given `consumer`, 54 | * otherwise executes `emptyAction` instead. 55 | * 56 | * @param consumer a consumer of the payload, if present 57 | * @param emptyAction an action, if empty 58 | */ 59 | abstract ifPresentOrElse(consumer: (value: T) => void, emptyAction: () => void): void; 60 | 61 | /** 62 | * Filters a payload with an additional `predicate`. 63 | * 64 | * If a payload is present and the payload matches the given `predicate`, returns `this`, 65 | * otherwise returns an empty `Optional` even if this is present. 66 | * 67 | * @param predicate a predicate to test the payload, if present 68 | */ 69 | abstract filter(predicate: (value: T) => boolean): Optional; 70 | 71 | /** 72 | * Maps a payload with a mapper. 73 | * 74 | * If a payload is present, returns an `Optional` as if applying `Optional.ofNullable` to the result of 75 | * applying the given `mapper` to the payload, 76 | * otherwise returns an empty `Optional`. 77 | * 78 | * @param mapper a mapper to apply the payload, if present 79 | */ 80 | abstract map(mapper: (value: T) => U): Optional>; 81 | 82 | /** 83 | * Maps a payload with a mapper which returns Optional as a result. 84 | * 85 | * If a payload is present, returns the result of applying the given `mapper` to the payload, 86 | * otherwise returns an empty `Optional`. 87 | * 88 | * @param mapper a mapper to apply the payload, if present 89 | */ 90 | abstract flatMap(mapper: (value: T) => Optional): Optional; 91 | 92 | /** 93 | * If a payload is present, returns `this`, 94 | * otherwise returns an `Optional` provided by the given `supplier`. 95 | * 96 | * @param supplier a supplier 97 | */ 98 | abstract or(supplier: () => Optional): Optional; 99 | 100 | /** 101 | * If a payload is present, returns the payload, otherwise returns `another`. 102 | * 103 | * @param another an another value 104 | */ 105 | abstract orElse(another: T): T; 106 | 107 | /** 108 | * If a payload is present, returns the payload, 109 | * otherwise returns the result provided by the given `supplier`. 110 | * 111 | * @param supplier a supplier of another value 112 | */ 113 | abstract orElseGet(supplier: () => T): T; 114 | 115 | /** 116 | * If a payload is present, returns the payload, 117 | * otherwise throws an error provided by the given `errorSupplier`. 118 | * 119 | * @param errorSupplier a supplier of an error 120 | * @throws {T} when `this` is empty. 121 | */ 122 | abstract orElseThrow(errorSupplier: () => U): T; 123 | 124 | /** 125 | * If a payload is present, returns the payload, 126 | * otherwise returns `null`. 127 | */ 128 | abstract orNull(): T | null; 129 | 130 | /** 131 | * If a payload is present, returns the payload, 132 | * otherwise returns `undefined`. 133 | */ 134 | abstract orUndefined(): T | undefined; 135 | 136 | /** 137 | * Converts this to an `Option`. 138 | */ 139 | abstract toOption(): Option; 140 | 141 | /** 142 | * Returns an appropriate result by emulating pattern matching with the given `cases`. 143 | * If a payload is present, returns the result of `present` case, 144 | * otherwise returns the result of `empty` case. 145 | * 146 | * @param cases cases for this `Optional` 147 | */ 148 | abstract matches(cases: Cases): U; 149 | 150 | /** 151 | * This method is called by JSON.stringify automatically. 152 | * When a payload is present, it will be serialized as the payload itself, 153 | * otherwise, it will be serialized as `null`. 154 | * 155 | * @param key property name 156 | * @see https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior 157 | */ 158 | abstract toJSON(key: string): unknown; 159 | 160 | /** 161 | * Returns an Optional whose payload is the given non-null `value`. 162 | * 163 | * @param value a value 164 | * @throws {TypeError} when the given `value` is `null` or `undefined`. 165 | */ 166 | static of(value: T): Optional { 167 | if (value !== null && value !== undefined) 168 | return new PresentOptional(value); 169 | else 170 | throw new TypeError("The passed value was null or undefined."); 171 | } 172 | 173 | /** 174 | * This method is an alias of `Optional.of`. 175 | * 176 | * @param value a value 177 | * @throws {TypeError} when the given `value` is `null` or `undefined`. 178 | */ 179 | static ofNonNull(value: T): Optional { 180 | return Optional.of(value); 181 | } 182 | 183 | /** 184 | * If the given `nullable` value is not `null` or `undefined`, 185 | * returns an `Optional` whose payload is the given value, 186 | * otherwise (or when `null` or `undefined`) returns an empty `Optional`. 187 | * 188 | * @param nullable a nullable value 189 | */ 190 | static ofNullable(nullable: T | null | undefined): Optional { 191 | if (nullable !== null && nullable !== undefined) 192 | return new PresentOptional(nullable); 193 | else 194 | return new EmptyOptional(); 195 | } 196 | 197 | /** 198 | * Returns an empty `Optional`. 199 | */ 200 | static empty(): Optional { 201 | return new EmptyOptional(); 202 | } 203 | 204 | /** 205 | * Retrieve the given `option` as an Optional. 206 | * 207 | * @param option an `Option` object to retrieve 208 | * @throws {TypeError} when the given `option` does not have a valid `kind` attribute. 209 | */ 210 | static from(option: Option): Optional { 211 | switch (option.kind) { 212 | case "present": return Optional.of(option.value); 213 | case "empty": return Optional.empty(); 214 | default: throw new TypeError("The passed value was not an Option type."); 215 | } 216 | } 217 | } 218 | 219 | class PresentOptional extends Optional { 220 | payload: T; 221 | 222 | isPresent(): boolean { 223 | return true; 224 | } 225 | 226 | constructor(value: T) { 227 | super(); 228 | this.payload = value; 229 | } 230 | 231 | get(): T { 232 | return this.payload; 233 | } 234 | 235 | ifPresent(consumer: (value: T) => void): void { 236 | consumer(this.payload); 237 | } 238 | 239 | ifPresentOrElse(consumer: (value: T) => void, emptyAction: () => void): void { 240 | consumer(this.payload); 241 | } 242 | 243 | filter(predicate: (value: T) => boolean): Optional { 244 | return (predicate(this.payload)) ? this : Optional.empty(); 245 | } 246 | 247 | map(mapper: (value: T) => U): Optional> { 248 | const result: U = mapper(this.payload); 249 | return Optional.ofNullable(result!); 250 | } 251 | 252 | flatMap(mapper: (value: T) => Optional): Optional { 253 | return mapper(this.payload); 254 | } 255 | 256 | or(supplier: () => Optional): Optional { 257 | return this; 258 | } 259 | 260 | orElse(another: T): T { 261 | return this.payload; 262 | } 263 | 264 | orElseGet(another: () => T): T { 265 | return this.payload; 266 | } 267 | 268 | orElseThrow(exception: () => U): T { 269 | return this.payload; 270 | } 271 | 272 | orNull(): T { 273 | return this.payload; 274 | } 275 | 276 | orUndefined(): T { 277 | return this.payload; 278 | } 279 | 280 | toOption(): Option { 281 | return { kind: "present", value: this.payload }; 282 | } 283 | 284 | matches(cases: Cases): U { 285 | return cases.present(this.payload); 286 | } 287 | 288 | toJSON(key: string): unknown { 289 | return this.payload; 290 | } 291 | } 292 | 293 | class EmptyOptional extends Optional { 294 | isPresent(): boolean { 295 | return false; 296 | } 297 | 298 | constructor() { 299 | super(); 300 | } 301 | 302 | get(): T { 303 | throw new TypeError("The optional is not present."); 304 | } 305 | 306 | ifPresent(consumer: (value: T) => void): void { 307 | } 308 | 309 | ifPresentOrElse(consumer: (value: T) => void, emptyAction: () => void): void { 310 | emptyAction(); 311 | } 312 | 313 | filter(predicate: (value: T) => boolean): Optional { 314 | return this; 315 | } 316 | 317 | map(mapper: (value: T) => U): Optional> { 318 | return Optional.empty(); 319 | } 320 | 321 | flatMap(mapper: (value: T) => Optional): Optional { 322 | return Optional.empty(); 323 | } 324 | 325 | or(supplier: () => Optional): Optional { 326 | return supplier(); 327 | } 328 | 329 | orElse(another: T): T { 330 | return another; 331 | } 332 | 333 | orElseGet(another: () => T): T { 334 | return this.orElse(another()); 335 | } 336 | 337 | orElseThrow(exception: () => U): T { 338 | throw exception(); 339 | } 340 | 341 | orNull(): null { 342 | return null; 343 | } 344 | 345 | orUndefined(): undefined { 346 | return undefined; 347 | } 348 | 349 | toOption(): Option { 350 | return { kind: "empty" }; 351 | } 352 | 353 | matches(cases: Cases): U { 354 | return cases.empty(); 355 | } 356 | 357 | toJSON(key: string): unknown { 358 | return null; 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An interface that represents respective cases of pattern matching of `Optional`. 3 | */ 4 | export interface Cases { 5 | /** 6 | * A mapper that maps a payload for case of an `Optional` is present. 7 | */ 8 | present: (value: T) => U; 9 | 10 | /** 11 | * A supplier that provides a value for case of an `Optional` is empty. 12 | */ 13 | empty: () => U; 14 | } 15 | 16 | /** 17 | * An alias of algebraic, prototype-free JavaScript object which represents `Optional`. 18 | * Objects of this type are provided by `Optional.toOption` 19 | * and they can be retrieved as `Optional` by `Optional.from`. 20 | */ 21 | export type Option = Present | Empty; 22 | 23 | export interface Present { 24 | kind: "present"; 25 | value: T; 26 | } 27 | 28 | export interface Empty { 29 | kind: "empty"; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ES2019", 5 | "module": "ESNext", 6 | "outDir": "dist/esm/", 7 | "rootDirs": [ 8 | "src", 9 | "test" 10 | ], 11 | "sourceMap": true, 12 | "declaration": true, 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true 16 | }, 17 | "include": [ 18 | "src/**/*.ts" 19 | ], 20 | "exclude": [ 21 | "dist", 22 | "node_modules", 23 | "src/**/*.spec.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended"], 3 | "rules": { 4 | "arrow-parens": [true, "ban-single-arg-parens"], 5 | "curly": false, 6 | "interface-name": false, 7 | "no-empty": [true, "allow-empty-functions"], 8 | "no-trailing-whitespace": false, 9 | "max-classes-per-file": false, 10 | "member-access": false, 11 | "member-ordering": false, 12 | "object-literal-sort-keys": false 13 | } 14 | } --------------------------------------------------------------------------------